Обновить доки по закрытию остатков код-стайла
Аудит §2 переписан под решения (var-гейт, LF, дедуп закрыт — дублей нет); backlog: TD-COMMENTS-IFACE п.1–3 закрыты, TD-STYLE-ANALYZERS закрыт; STATUS.md — новый блок захода, устаревший блок «Осталось (в backlog)» в шапке удалён; план и ledger захода.
This commit is contained in:
+221
-212
@@ -1,212 +1,221 @@
|
||||
# Дейл (Deal) — Статус разработки и прогресс
|
||||
|
||||
> Обновляется в конце каждого захода. Проект в git (ветка `main`, коммиты на русском) — борд состояния + git-история.
|
||||
> Дата последнего обновления: 2026-09-11.
|
||||
>
|
||||
> **2026-09-11 — единый контракт источника (generic source).** Ядро (домен Cards, Storage-сервис,
|
||||
> персистентность, конвейер, wire, фронт) переведено с Telegram-полей карточки на generic-тип
|
||||
> `SourceItem` (`SourceRef` + `SourceContent`, вложения — `DataRef` → общий Storage). Дашборды/канбан/конвейер
|
||||
> больше не знают о Telegram; Telegram-специфика — только в тонком адаптере приёма. Tenant-миграции
|
||||
> пересозданы с нуля (init). Добавлены extension-point `ISourceContentProvider`/`SourceContentResolver` и
|
||||
> `GET /api/cards/{id}/source`. Входящий поток источников — generic (`sources.proto`/`PushSource`,
|
||||
> `SourceIngressGrpcService`), `PushMessage` из telegram.proto удалён. Сухой прогон текста по конвейеру
|
||||
> (стоп-правила → ML → ИИ) без записи: `POST /api/admin/check-message` + UI настроек. Remote-просмотр
|
||||
> исходника: `TelegramService.ReadSource` + `TelegramSourceContentProvider` + UI «Обновить из источника».
|
||||
> Метрика алертинга `deal.ai.budget.used.ratio{tenant}`; actor/tenant в access-логе; прогресс переклассификации
|
||||
> через SSE. Hardening контейнеров (non-root/read-only/limits), шардированная пакетная миграция схем,
|
||||
> единый CI (`scripts/ci.sh`). Ядро: build 5 sln 0/0, `Deal.Tests.Unit` **1326/1326 PASS**,
|
||||
> telegram **130/130**, ai **52/52**, ml **38/38**, storage **9/9**, фронт `build` + `lint:i18n` зелёные.
|
||||
> Детали — `docs/superpowers/specs/2026-09-11-source-contract-design.md`.
|
||||
> Осталось (в backlog): `GET /api/cards/{id}/source` + `ISourceContentProvider`, выгрузка вложений
|
||||
> telegram-адаптером в Storage, `TelegramSourceContentProvider`, перенос оставшейся Telegram-специфики
|
||||
> (`TelegramStore`, `Dialogs`/`TgMessages`, Discovery) в telegram-сервис.
|
||||
|
||||
**Все этапы 0–12 выполнены (100%)** — см. roadmap
|
||||
> `docs/superpowers/plans/2026-09-05-deal-roadmap.md`, план этапа 10
|
||||
> `docs/superpowers/plans/2026-09-10-deal-stage10-operator-analytics.md` и ledgers в `.superpowers/sdd/`.
|
||||
> Live-приёмки на Docker Desktop выполнены: dev-smoke 14/14, SaaS-контур 15/15, prod-контур+mTLS+
|
||||
> observability PASS, backup/restore на копии PASS, runtime-приёмка этапа 10 (оператор-консоль,
|
||||
> аналитика, аудит) PASS (см. чек-лист ниже). Осталось Manual: реальные Telegram/LLM-креды (п.5).
|
||||
|
||||
## Общий прогресс по этапам
|
||||
|
||||
| Этап | Статус | Задач | Тесты (unit, накопительно) | Приёмка |
|
||||
|---|---|---|---|---|
|
||||
| 0. Каркас | ✅ готов | 9/9 | 6 | health 200 |
|
||||
| 1. Доступ и мультитенантность | ✅ готов | 6/6 | 25 | auth 1:1 |
|
||||
| 2. Settings (настройки) | ✅ готов | 11/11 | 175 | 60/60 |
|
||||
| 3. Kanban (дашборд) | ✅ готов | 15/15 | 410 | 94/94 |
|
||||
| 4. Pipeline/«Обработка» | ✅ готов | 13/13 | 535 | 74/74 |
|
||||
| 5. Projects («Выбранные») | ✅ готов | 13/13 | 620 | 75/75 |
|
||||
| 6. Сервисы telegram/ml/ai + Discovery | ✅ готов | 20/20 | 830 | 20/20 + 37/37 |
|
||||
| 7. SaaS-контур (оператор/инвайты/лимиты/аудит/безопасность/prod-деплой/бэкапы/доки) | ✅ готов | 16/16 | 1123 | ✅ live SaaS 15/15 (остальное — ⚠ Manual) |
|
||||
| 8. Code-quality rework (ревью 5 зон) | ✅ готов | 5/5 фаз | 1139 | build 4 sln 0/0; фронт build OK |
|
||||
| 9. Единая карточка (слияние Kanban/Projects, `/api/cards`+`/api/containers`) | ✅ готов | 11/11 | 1138 | ✅ live dev-smoke PASS=14 FAIL=0 |
|
||||
| 10. Оператор-консоль, аналитика, аудит действий, Grafana/Loki-дашборды | ✅ готов | 7/7 | 1173 | ✅ live runtime (Docker dev) |
|
||||
| 11. Локализация UI (вынос строк в ресурсы) | ✅ готов | 7/7 | 1173 | build + `lint:i18n` зелёные |
|
||||
| 12. Наблюдаемость/устойчивость/перф + добивка ТЗ | ✅ готов | 4/4 пакетов + добивка | 1275 | build 4 sln 0/0; telegram 125/125 |
|
||||
| **Итого** | **этапы 0–12 = 100%** | **137/137** | **1275 (core)** + 125/52/38 (сервисы) | — |
|
||||
|
||||
Финальный прогон этапа 12 (2026-09-10, автономный заход A–D): build `Deal.sln` 0/0; core **1275/1275 PASS**;
|
||||
telegram **125/125**; фронт `npm run build` зелёный (main-чанк 309 kB, словарь в отдельном чанке i18n),
|
||||
`npm run lint:i18n` зелёный; метрики: `/metrics` (OTel→Prometheus) во всех 4 процессах, Prometheus targets 5/5 UP;
|
||||
пакеты B (rate-limit/LoginAttemptGuard на Postgres, разлогин suspended, purge) и D (reclassify + токены ML)
|
||||
с зелёными тестами. Всё остановлено (правило «без хвостов»). LEDGER: `.superpowers/sdd/deal-stage12-observability-hardening/`.
|
||||
|
||||
Финальный прогон этапа 10 (2026-09-10): build `Deal.sln` 0/0; core **1173/1173 PASS**; фронт
|
||||
`npm run build` зелёный; `GET /api/operator/analytics/{overview,tokens,activity}` и
|
||||
`GET /api/operator/audit` живьём на dev-Postgres (`:5433`, миграция `AddTokenUsageEvents` применена),
|
||||
`deal-core` healthy; страницы `#/operator` и `#/join` отдаются dev-сервером. Детали — ledger
|
||||
`.superpowers/sdd/deal-stage10-operator-analytics/progress.md`.
|
||||
|
||||
Финальный прогон (Task 16, 2026-09-08, docker выключен): build 0 warnings / 0 errors всех четырёх sln
|
||||
(core/telegram/ai/ml); core 1123/1123 PASS, telegram 114/114, ai 50/50, ml 36/36 PASS;
|
||||
`docker compose -f deploy/compose.prod.yml config` rc=0 (+ профиль observability);
|
||||
`sh -n` scripts/dev-smoke.sh/backup.sh/restore.sh/mtls-certs.sh rc=0.
|
||||
> Накопительный счётчик core в таблице — по состоянию на конец этапа: этап 8 — 1139, этап 9 — 1138
|
||||
> (часть тестов удалена вместе с доменом Projects), этап 10 — 1173.
|
||||
|
||||
## Что система умеет СЕЙЧАС (проверяемо)
|
||||
|
||||
- **Ядро (этапы 0–6)**: вход admin/admin (dev-seed, dev-only), сессии, схемы на тенанта (Postgres :5433);
|
||||
дашборд (колонки/карточки/drag&drop/архив/корзина/FTS-поиск), «Обработка»
|
||||
(очередь→стоп-лист→дедуп→ML/ИИ→карточка), «Выбранные» (стадии/напоминания SSE/файлы Local/MinIO),
|
||||
Настройки (ключи AI enc:, промпты, валюты; Telegram-ключи — глобально у оператора), Каналы/Discovery; полный dev-стек этапа 6 —
|
||||
`deploy/compose.dev.yml` (`Services__*__UseLocal=false`, gRPC-режим telegram/ai/ml + ингресс core).
|
||||
- **SaaS-контур (этап 7)**: оператор (`public.operators/operator_sessions`, кука `deal_operator_session`,
|
||||
bootstrap env `DEAL_OPERATOR_*`; dev-дефолт operator/operator) и ручки `/api/operator/*`
|
||||
(auth/tenants/invites/limits/audit/health — с этапа 10 у них есть UI, см. ниже); инвайты (код 16 симв., 72 ч) и активация
|
||||
`POST /api/join` (пользователь + провижининг тенанта); лимиты ИИ-бюджета (`tenant_limits`,
|
||||
`TokenUsageRecorder`, гейт-декораторы → Local-фолбэк, SSE-тосты 80/100%); append-only аудит;
|
||||
rate limiting (auth 10/мин·IP, api 600/мин·тенант, gRPC-ингресс 600/мин·тенант, `LoginAttemptGuard`
|
||||
5/15 мин); Origin-проверка мутаций + security-заголовки + ForwardedHeaders за Caddy;
|
||||
mTLS за флагом `DEAL_MTLS_*` (`scripts/mtls-certs.sh` → `deploy/certs/`); Serilog JSON во всех
|
||||
4 процессах (+ access-логи HTTP/gRPC); prod-деплой `deploy/compose.prod.yml` (caddy 80/443,
|
||||
postgres/minio без host-портов, профиль observability: promtail/loki/grafana, `.env.prod.example`);
|
||||
бэкапы `scripts/backup.sh`/`restore.sh`/`deal-backup-lib.sh` (pg_dump -Fc + MinIO + tar; retention 14).
|
||||
- **Оператор-консоль и аналитика (этап 10)**: hash-роутер фронта (`#/` приложение, `#/operator` консоль,
|
||||
`#/join?code=…` активация инвайта); разделы консоли (вход, тенанты+suspend/resume/impersonate,
|
||||
инвайты, лимиты, аудит с фильтрами/пагинацией, аналитика, health); impersonation ставит httpOnly-куку
|
||||
`deal_session` тем же ответом (оператор сразу в тенанте). Сквозной аудит действий (`public.audit_log`):
|
||||
выходы, `invite_joined`, действия карточек/комментариев, CRUD контейнеров, настройки, каналы, Telegram.
|
||||
История расхода токенов `public.token_usage_events` + операторская аналитика
|
||||
(`/api/operator/analytics/{overview,tokens,activity}`, `groupBy=day|tenant|provider|model`).
|
||||
Grafana provisioning (datasource Loki + дашборды `Deal-Auth/Errors/Rps/Logs`) и promtail-лейблы.
|
||||
- **Что увидеть глазами**: полный dev-стек — `docker compose -f deploy/compose.dev.yml up -d --build`
|
||||
→ фронт `cd src/frontend && npm run dev` → логин `admin/admin` (dev-seed);
|
||||
либо сквозной smoke одной командой: `sh scripts/dev-smoke.sh`. Prod-контур — §13.8 техдока
|
||||
(оператор → тенант → инвайт → `/api/join`). Host-режим (Local-заглушки): postgres/minio +
|
||||
`dotnet run --project src/core/Deal.Api --urls http://localhost:5080`.
|
||||
- Полная карта «что/где» — техдок `docs/technical/Техническая-документация-Дейл.md` (§5, §7–§11,
|
||||
§13.1–§13.10), api-map `docs/api/api-map.md` (раздел «Реализовано в Deal»), инструкция пользователя
|
||||
`docs/user-guide/Инструкция-пользователя-Дейл.md`.
|
||||
|
||||
## Manual-чек-лист (остаток после live-приёмок)
|
||||
|
||||
Выполнено живьём на Docker (автономно от авто-прогонов Task 16, build/test/config/syntax — там же):
|
||||
|
||||
1. ✅ **SaaS-сквозная приёмка (live, 15/15 PASS)** — `run-live-saas.sh` + `live-saas-check.sh` на поднятом
|
||||
dev-Postgres (:5433, миграции SystemSaaS + SessionsImpersonationMark применены): оператор login →
|
||||
создать тенанта → инвайт → `POST /api/join` → вход пользователя → settings/cards (на тот момент —
|
||||
`boards`/демо-карточка) →
|
||||
IDOR-негатив 401 (пользователь к операторским ручкам) → suspend (вход 403) → resume (вход 200) →
|
||||
лимиты (tenant_limits) → аудит-лента. Core погашен, :5080 свободен.
|
||||
2. ✅ **dev-smoke 14/14 PASS** — полный gRPC-стек (`scripts/dev-smoke.sh`): подъём, health, login
|
||||
admin/admin, `/api/tg/status` idle, `POST /api/cards` (карточка `planned`) → trash → обучающий сигнал spam,
|
||||
ML-флашер выгрузил outbox. Стек погашен скриптом (trap).
|
||||
3. ✅ **Prod-контур + mTLS + observability (live)** — сертификаты перегенерированы (`scripts/mtls-certs.sh -f`,)
|
||||
полный набор в deploy/certs; подъём `compose.prod.yml` + `--profile observability` с фиктивными
|
||||
env-секретами (`DEAL_MTLS_ENABLED=1`, endpoint'ы https://): core/telegram/ai/ml **healthy** под mTLS;
|
||||
исходящее mTLS подтверждено живьём — `/api/tg/status` (idle) и `/api/ml/status` (reachable:true) через
|
||||
Caddy; фронт и `/api/health` через Caddy 200; promtail→loki (логи пишутся), Grafana 200. Исправлен
|
||||
дефект `deploy/observability/loki.yml` (Loki 3.x: `delete_request_store`). `.env.prod` тестовый удалён.
|
||||
4. ✅ **backup/restore (live, на копии)** — `backup.sh`: pg (-Fc) + minio (docker-mc) + data (tar docker-томов)
|
||||
+ retention; `restore.sh pg` в копию-БД — 43 таблицы/3 схемы идентичны, данные сошлись (users=2,
|
||||
tenants=2, sessions=30); `restore.sh minio` с реальным объектом (залит→бэкап→удалён→восстановлен);
|
||||
`restore.sh data`. Исправлены дефекты скриптов, проявившиеся живьём: двойная схема в MC_HOST_deal
|
||||
(`deal-backup-lib.sh`), пустой бакет → mv (`backup.sh`), Windows/MSYS docker-пути (`host_docker_path`).
|
||||
5. ❌ Реальный Telegram-вход (api_id/api_hash/QR) и LLM-вызовы — **нужны живые креды**.
|
||||
6. ✅ Прогон `scripts/backup.sh` и restore-тест — см. п.4 (полный цикл на dev-хранилищах и копии-БД).
|
||||
7. ✅ **Этап 10 — runtime-приёмка (Docker dev-стек)** — миграция `AddTokenUsageEvents` применена к
|
||||
dev-Postgres (`:5433`), `deal-core` пересобран/healthy; операторский вход `operator`/`operator` → 200;
|
||||
`GET /api/operator/tenants`, `/audit`, `/analytics/overview`, `/analytics/tokens?groupBy=day|provider`,
|
||||
`/analytics/activity?limit=3` → 200 (реальные лента/агрегаты); фронт `npm run build` зелёный, dev-сервер
|
||||
отдаёт `#/operator` и `#/join`; core-тесты 1173/1173. Детали — ledger этапа 10.
|
||||
|
||||
## Заделы (этап 13+; подробно — техдок §11 и roadmap)
|
||||
|
||||
> **Единый источник отложенного и техдолга — `backlog.md` в корне.** Ниже — краткая выжимка.
|
||||
|
||||
- **Этап 11 — Локализация интерфейса (i18n)** — **выполнен** (2026-09-10, урезанный объём): все
|
||||
пользовательские строки фронта в ресурсах (`src/frontend/src/i18n/`, 1039 ключей в 13 областях),
|
||||
линтер `npm run lint:i18n`. Переключатель языка и второй язык — **в бэклоге**: делаем, когда появится
|
||||
потребность (ядро i18n/`registerLocale` к этому готово).
|
||||
- **Этап 12 — Наблюдаемость/устойчивость/перф** — **выполнен** (2026-09-10, автономно, пакеты A–D):
|
||||
метрики Prometheus+Grafana (`/metrics` :9464 во всех процессах); распределённый rate-limit и
|
||||
`LoginAttemptGuard` на Postgres; мгновенный разлогин suspended-сессий; авто-purge `audit_log`/`tenant_limits`;
|
||||
разбиение бандла фронта + прогрессивный рендер колонок; LRU-кэши WTelegram; пакетная миграция схем тенантов;
|
||||
реальный `reclassify` с Local-фолбэком. LEDGER: `.superpowers/sdd/deal-stage12-observability-hardening/`.
|
||||
- **Остатки этапа 12 (закрыто 2026-09-10):** доки под этап 12 (real-reclassify, maintenance-migrate, без
|
||||
демо), Prometheus alert rules (`deploy/observability/prometheus-rules.yml` + провижининг), устранена гонка
|
||||
`FreeTcpPort()` в тест-харнессе (единый `TestPort`), SSE `cards_reclassified` (бэк+фронт), нагрузочные
|
||||
скрипты `scripts/loadtest/`, скан уязвимостей — **чисто** (core: 0 уязвимых пакетов; frontend `npm audit`: 0).
|
||||
- Прочее (требует владельца/кредов): биллинг/провайдер планов и саморегистрация; мультиаккаунтность Telegram;
|
||||
k8s/Cloudflare; Kafka; экспорт/импорт ML; переключатель языка/второй язык (в бэклоге — по потребности);
|
||||
реальный Telegram-вход и живые LLM-вызовы;
|
||||
legacy-прототип `docker-compose.yml` перенесён в `archive/leadradar-legacy/` (2026-09-10).
|
||||
- **Добивка по ТЗ (2026-09-10, автономно)** — закрыты найденные аудитом частично/незакрытые пункты:
|
||||
ML-проверка на канале/сообщении (`/api/ml/candidates`+`/apply` — реальные, не заглушки); глобальные
|
||||
исключения до ML/ИИ (§5.14); новые группы фильтров колонки `levels/locations/types/prices` (§6.3);
|
||||
«открыть исходник» как быстрое действие на карточке (§6.6); глубины очередей/сессии в `/api/operator/health` (§10.2);
|
||||
детектор подозрительной активности `/api/operator/analytics/suspicious` (§10.5); раздел настроек и
|
||||
поддержка тем «Внешний вид» — тёмная (дефолт) / светлая / системная (§8.12). Отчёт аудита:
|
||||
`docs/superpowers/reviews/2026-09-10-tz-compliance-audit.md`. Core-тесты **1275/1275**; фронт build+`lint:i18n` зелёные.
|
||||
- **Telegram-ключи — вариант A (2026-09-10, по решению владельца):** `api_id`/`api_hash` задаёт
|
||||
**оператор глобально** (таблица `public.global_settings`, ручки `GET/PUT /api/operator/settings/telegram-keys`,
|
||||
hash шифруется; раздел «Telegram» в оператор-консоли). У тенанта ключи убраны — только подключение
|
||||
аккаунта; без ключей подключение недоступно (`/api/tg/status → keysSet:false`). Миграция `GlobalSettings`
|
||||
**применена**; `PUT` поддерживает частичное обновление (можно сменить одно поле). Core-тесты **1275/1275**.
|
||||
|
||||
## Code-quality rework (2026-09-08, после ревью 5 зон ~1100 файлов)
|
||||
|
||||
Проведено многоосевое ревью (безопасность/корректность/архитектура/перф) бэкенда и фронта; findings —
|
||||
`docs/superpowers/reviews/2026-09-08-code-quality-review.md`. Все исправления закрыты и проверены:
|
||||
**core 1135/1135, telegram 118/118, ai 52/52, ml 38/38**, фронт build OK, 4 sln 0/0. Главное:
|
||||
|
||||
- **Безопасность**: SSRF-гейт (baseUrl каталоговых провайдеров фиксирован + private-IP-блок в проверке),
|
||||
fail-closed Production (rate limit/CORS/conn-string обязательны), код инвайта в аудите — SHA-256, пароль ≥8,
|
||||
маска ключа не перезаписывает ключ и короткие секреты скрыты, TenantId = 32-hex, mTLS fail-closed,
|
||||
gRPC-лимиты входных данных, join проверяет целевого тенанта, атомарный инкремент токенов (Npgsql).
|
||||
- **Корректность**: атомарные append комментариев/ссылок/файлов (1 SQL), уникальный objectKey, атомарный
|
||||
дедуп-pump, запрет move из trash/archive/taken, логирование «немых» catch, очистка сессий вне hot-path;
|
||||
фронт: смена пароля с текущим паролем, boot не падает от одного 500, автосейв не затирает промпты,
|
||||
гонки поиска закрыты seq-токенами.
|
||||
- **Архитектура**: `Deal.Grpc.Hosting` (общая обвязка 3 сервисов), `TenantSettingsSnapshot` (9 копий чтения
|
||||
настроек → одна), декомпозиция 7 крупных файлов на partial (<350 строк), фронт store.js → слайсы `store/`,
|
||||
вынесены компоненты Settings/Discovery; мёртвый код удалён.
|
||||
- **Реестры констант (C35, 2026-09-09)**: общие `Deal.Contracts.Integrations.MlLearningLabels`
|
||||
(spam/t:hire/t:order) и `SourceDefaults` (DefaultHue) вместо дублей в 5 модулях; единый предикат «активные
|
||||
правила» (Kanban `ColumnRules.HasActiveRules`); реестр id-стадий `ProjectStages` (с этапа 9 — `CardsDefaultContainers`); общий
|
||||
`CardsService.JustNowLabel`; TTL-эвикция в DiscoverySearchErrorCounter (+4 теста, core 1139).
|
||||
- **Заделы** (не рисковали без e2e/не успели): вынос оставшихся вкладок SettingsView, Optional-пункты
|
||||
(пагинация колонок, виртуализация, LRU-кэши WTelegram и др.). Подробности — в отчёте ревью и
|
||||
`.superpowers/sdd/deal-stage8-quality-rework/`.
|
||||
|
||||
## Процесс (обязательства, чтобы не жрать память/хосты)
|
||||
|
||||
- Acceptance-серверы — только через враппер с гарантированным kill (taskkill //T //F по PID-файлу) +
|
||||
проверка освобождения порта; в конце каждой приёмки — шаг очистки.
|
||||
- Контейнеры — по требованию (`docker compose ... up|down`); между заходами ничего не держать;
|
||||
`scripts/cleanup-dev.sh` (kill висящих Deal.*/тест-хостов, `dotnet build-server shutdown`,
|
||||
остановка deal-контейнеров) — в конце захода.
|
||||
- **Хвостов не оставлять (правило владельца, 2026-09-10):** по завершении работы все сервисы и процессы
|
||||
должны быть остановлены — включая Docker-контейнеры и dev-серверы (frontend/Vite, dotnet) — если
|
||||
владелец явно не попросил оставить их запущенными. Проверка в конце: `docker ps` без `deal-*`,
|
||||
свободные порты (5173/5080/5082/5101/5102/5103/5433/9000/9001/9464), `dotnet build-server shutdown`.
|
||||
- **Разовые решения — одним списком в начале (правило владельца, 2026-09-10):** все вопросы, требующие
|
||||
выбора владельца, собираются и задаются **сразу, до начала работы**, а не по ходу/в конце. **Не
|
||||
спрашивать о том, что уже определено ТЗ/принятыми решениями** — это делать без вопросов; вопрос —
|
||||
только при реальном противоречии в требованиях. При неоднозначности без противоречий — выбирать
|
||||
безопасный обратимый дефолт и делать (напр. перенос, а не удаление).
|
||||
- **Легаси-прототип LeadRadar** перенесён из корня в `archive/leadradar-legacy/` (2026-09-10; обратимо,
|
||||
на сборку/запуск не влияет).
|
||||
# Дейл (Deal) — Статус разработки и прогресс
|
||||
|
||||
> Обновляется в конце каждого захода. Проект в git (ветка `main`, коммиты на русском) — борд состояния + git-история.
|
||||
> Дата последнего обновления: 2026-09-11.
|
||||
>
|
||||
> **2026-09-11 — единый контракт источника (generic source).** Ядро (домен Cards, Storage-сервис,
|
||||
> персистентность, конвейер, wire, фронт) переведено с Telegram-полей карточки на generic-тип
|
||||
> `SourceItem` (`SourceRef` + `SourceContent`, вложения — `DataRef` → общий Storage). Дашборды/канбан/конвейер
|
||||
> больше не знают о Telegram; Telegram-специфика — только в тонком адаптере приёма. Tenant-миграции
|
||||
> пересозданы с нуля (init). Добавлены extension-point `ISourceContentProvider`/`SourceContentResolver` и
|
||||
> `GET /api/cards/{id}/source`. Входящий поток источников — generic (`sources.proto`/`PushSource`,
|
||||
> `SourceIngressGrpcService`), `PushMessage` из telegram.proto удалён. Сухой прогон текста по конвейеру
|
||||
> (стоп-правила → ML → ИИ) без записи: `POST /api/admin/check-message` + UI настроек. Remote-просмотр
|
||||
> исходника: `TelegramService.ReadSource` + `TelegramSourceContentProvider` + UI «Обновить из источника».
|
||||
> Метрика алертинга `deal.ai.budget.used.ratio{tenant}`; actor/tenant в access-логе; прогресс переклассификации
|
||||
> через SSE. Hardening контейнеров (non-root/read-only/limits), шардированная пакетная миграция схем,
|
||||
> единый CI (`scripts/ci.sh`). Ядро: build 5 sln 0/0, `Deal.Tests.Unit` **1326/1326 PASS**,
|
||||
> telegram **130/130**, ai **52/52**, ml **38/38**, storage **9/9**, фронт `build` + `lint:i18n` зелёные.
|
||||
> Детали — `docs/superpowers/specs/2026-09-11-source-contract-design.md`.
|
||||
>
|
||||
> **2026-09-11 (вечер) — закрыты остатки код-стайла (TD-COMMENTS-IFACE, TD-STYLE-ANALYZERS).** Дедупликация
|
||||
> `<summary>`: дублей нет (сканы по тексту и по имени члена — 39 интерфейсов/229 членов). `var`: гейт
|
||||
> `csharp_style_var_for_built_in_types = false:warning` (ломает сборку), остаток выправлен `dotnet format`
|
||||
> по 5 sln (51 файл), «очевидный/прочий тип» — silent осознанно. Переводы строк: решено LF — `.gitattributes`
|
||||
> (`* text=auto eol=lf`), `.editorconfig` → lf, нормализовано 1029 файлов; попутно починены 42 CRLF-.sh
|
||||
> (первый прогон удалённого CI падал бы). Понижено 12 новых private XML-доков; добавлены 4 `<summary>`
|
||||
> членам интерфейсов; переведены 3 англоязычных комментария; из индекса убраны 2 `__pycache__/*.pyc`;
|
||||
> STATUS.md — удалён устаревший блок «Осталось (в backlog)» в шапке. Явные реализации интерфейсов (§11) —
|
||||
> остались точечным ревью владельца (43 интерфейса с реализациями, массовая правка не автоматизируется).
|
||||
> Сборка 5 sln 0/0; тесты: core **1340/1340**, telegram **130/130**, ai **52/52**, ml **38/38**, storage **9/9** — зелёные.
|
||||
> Детали — `docs/spec/Код-стайл-аудит-2026-09-11.md`, §2.
|
||||
|
||||
**Все этапы 0–12 выполнены (100%)** — см. roadmap
|
||||
> `docs/superpowers/plans/2026-09-05-deal-roadmap.md`, план этапа 10
|
||||
> `docs/superpowers/plans/2026-09-10-deal-stage10-operator-analytics.md` и ledgers в `.superpowers/sdd/`.
|
||||
> Live-приёмки на Docker Desktop выполнены: dev-smoke 14/14, SaaS-контур 15/15, prod-контур+mTLS+
|
||||
> observability PASS, backup/restore на копии PASS, runtime-приёмка этапа 10 (оператор-консоль,
|
||||
> аналитика, аудит) PASS (см. чек-лист ниже). Осталось Manual: реальные Telegram/LLM-креды (п.5).
|
||||
|
||||
## Общий прогресс по этапам
|
||||
|
||||
| Этап | Статус | Задач | Тесты (unit, накопительно) | Приёмка |
|
||||
|---|---|---|---|---|
|
||||
| 0. Каркас | ✅ готов | 9/9 | 6 | health 200 |
|
||||
| 1. Доступ и мультитенантность | ✅ готов | 6/6 | 25 | auth 1:1 |
|
||||
| 2. Settings (настройки) | ✅ готов | 11/11 | 175 | 60/60 |
|
||||
| 3. Kanban (дашборд) | ✅ готов | 15/15 | 410 | 94/94 |
|
||||
| 4. Pipeline/«Обработка» | ✅ готов | 13/13 | 535 | 74/74 |
|
||||
| 5. Projects («Выбранные») | ✅ готов | 13/13 | 620 | 75/75 |
|
||||
| 6. Сервисы telegram/ml/ai + Discovery | ✅ готов | 20/20 | 830 | 20/20 + 37/37 |
|
||||
| 7. SaaS-контур (оператор/инвайты/лимиты/аудит/безопасность/prod-деплой/бэкапы/доки) | ✅ готов | 16/16 | 1123 | ✅ live SaaS 15/15 (остальное — ⚠ Manual) |
|
||||
| 8. Code-quality rework (ревью 5 зон) | ✅ готов | 5/5 фаз | 1139 | build 4 sln 0/0; фронт build OK |
|
||||
| 9. Единая карточка (слияние Kanban/Projects, `/api/cards`+`/api/containers`) | ✅ готов | 11/11 | 1138 | ✅ live dev-smoke PASS=14 FAIL=0 |
|
||||
| 10. Оператор-консоль, аналитика, аудит действий, Grafana/Loki-дашборды | ✅ готов | 7/7 | 1173 | ✅ live runtime (Docker dev) |
|
||||
| 11. Локализация UI (вынос строк в ресурсы) | ✅ готов | 7/7 | 1173 | build + `lint:i18n` зелёные |
|
||||
| 12. Наблюдаемость/устойчивость/перф + добивка ТЗ | ✅ готов | 4/4 пакетов + добивка | 1275 | build 4 sln 0/0; telegram 125/125 |
|
||||
| **Итого** | **этапы 0–12 = 100%** | **137/137** | **1275 (core)** + 125/52/38 (сервисы) | — |
|
||||
|
||||
Финальный прогон этапа 12 (2026-09-10, автономный заход A–D): build `Deal.sln` 0/0; core **1275/1275 PASS**;
|
||||
telegram **125/125**; фронт `npm run build` зелёный (main-чанк 309 kB, словарь в отдельном чанке i18n),
|
||||
`npm run lint:i18n` зелёный; метрики: `/metrics` (OTel→Prometheus) во всех 4 процессах, Prometheus targets 5/5 UP;
|
||||
пакеты B (rate-limit/LoginAttemptGuard на Postgres, разлогин suspended, purge) и D (reclassify + токены ML)
|
||||
с зелёными тестами. Всё остановлено (правило «без хвостов»). LEDGER: `.superpowers/sdd/deal-stage12-observability-hardening/`.
|
||||
|
||||
Финальный прогон этапа 10 (2026-09-10): build `Deal.sln` 0/0; core **1173/1173 PASS**; фронт
|
||||
`npm run build` зелёный; `GET /api/operator/analytics/{overview,tokens,activity}` и
|
||||
`GET /api/operator/audit` живьём на dev-Postgres (`:5433`, миграция `AddTokenUsageEvents` применена),
|
||||
`deal-core` healthy; страницы `#/operator` и `#/join` отдаются dev-сервером. Детали — ledger
|
||||
`.superpowers/sdd/deal-stage10-operator-analytics/progress.md`.
|
||||
|
||||
Финальный прогон (Task 16, 2026-09-08, docker выключен): build 0 warnings / 0 errors всех четырёх sln
|
||||
(core/telegram/ai/ml); core 1123/1123 PASS, telegram 114/114, ai 50/50, ml 36/36 PASS;
|
||||
`docker compose -f deploy/compose.prod.yml config` rc=0 (+ профиль observability);
|
||||
`sh -n` scripts/dev-smoke.sh/backup.sh/restore.sh/mtls-certs.sh rc=0.
|
||||
> Накопительный счётчик core в таблице — по состоянию на конец этапа: этап 8 — 1139, этап 9 — 1138
|
||||
> (часть тестов удалена вместе с доменом Projects), этап 10 — 1173.
|
||||
|
||||
## Что система умеет СЕЙЧАС (проверяемо)
|
||||
|
||||
- **Ядро (этапы 0–6)**: вход admin/admin (dev-seed, dev-only), сессии, схемы на тенанта (Postgres :5433);
|
||||
дашборд (колонки/карточки/drag&drop/архив/корзина/FTS-поиск), «Обработка»
|
||||
(очередь→стоп-лист→дедуп→ML/ИИ→карточка), «Выбранные» (стадии/напоминания SSE/файлы Local/MinIO),
|
||||
Настройки (ключи AI enc:, промпты, валюты; Telegram-ключи — глобально у оператора), Каналы/Discovery; полный dev-стек этапа 6 —
|
||||
`deploy/compose.dev.yml` (`Services__*__UseLocal=false`, gRPC-режим telegram/ai/ml + ингресс core).
|
||||
- **SaaS-контур (этап 7)**: оператор (`public.operators/operator_sessions`, кука `deal_operator_session`,
|
||||
bootstrap env `DEAL_OPERATOR_*`; dev-дефолт operator/operator) и ручки `/api/operator/*`
|
||||
(auth/tenants/invites/limits/audit/health — с этапа 10 у них есть UI, см. ниже); инвайты (код 16 симв., 72 ч) и активация
|
||||
`POST /api/join` (пользователь + провижининг тенанта); лимиты ИИ-бюджета (`tenant_limits`,
|
||||
`TokenUsageRecorder`, гейт-декораторы → Local-фолбэк, SSE-тосты 80/100%); append-only аудит;
|
||||
rate limiting (auth 10/мин·IP, api 600/мин·тенант, gRPC-ингресс 600/мин·тенант, `LoginAttemptGuard`
|
||||
5/15 мин); Origin-проверка мутаций + security-заголовки + ForwardedHeaders за Caddy;
|
||||
mTLS за флагом `DEAL_MTLS_*` (`scripts/mtls-certs.sh` → `deploy/certs/`); Serilog JSON во всех
|
||||
4 процессах (+ access-логи HTTP/gRPC); prod-деплой `deploy/compose.prod.yml` (caddy 80/443,
|
||||
postgres/minio без host-портов, профиль observability: promtail/loki/grafana, `.env.prod.example`);
|
||||
бэкапы `scripts/backup.sh`/`restore.sh`/`deal-backup-lib.sh` (pg_dump -Fc + MinIO + tar; retention 14).
|
||||
- **Оператор-консоль и аналитика (этап 10)**: hash-роутер фронта (`#/` приложение, `#/operator` консоль,
|
||||
`#/join?code=…` активация инвайта); разделы консоли (вход, тенанты+suspend/resume/impersonate,
|
||||
инвайты, лимиты, аудит с фильтрами/пагинацией, аналитика, health); impersonation ставит httpOnly-куку
|
||||
`deal_session` тем же ответом (оператор сразу в тенанте). Сквозной аудит действий (`public.audit_log`):
|
||||
выходы, `invite_joined`, действия карточек/комментариев, CRUD контейнеров, настройки, каналы, Telegram.
|
||||
История расхода токенов `public.token_usage_events` + операторская аналитика
|
||||
(`/api/operator/analytics/{overview,tokens,activity}`, `groupBy=day|tenant|provider|model`).
|
||||
Grafana provisioning (datasource Loki + дашборды `Deal-Auth/Errors/Rps/Logs`) и promtail-лейблы.
|
||||
- **Что увидеть глазами**: полный dev-стек — `docker compose -f deploy/compose.dev.yml up -d --build`
|
||||
→ фронт `cd src/frontend && npm run dev` → логин `admin/admin` (dev-seed);
|
||||
либо сквозной smoke одной командой: `sh scripts/dev-smoke.sh`. Prod-контур — §13.8 техдока
|
||||
(оператор → тенант → инвайт → `/api/join`). Host-режим (Local-заглушки): postgres/minio +
|
||||
`dotnet run --project src/core/Deal.Api --urls http://localhost:5080`.
|
||||
- Полная карта «что/где» — техдок `docs/technical/Техническая-документация-Дейл.md` (§5, §7–§11,
|
||||
§13.1–§13.10), api-map `docs/api/api-map.md` (раздел «Реализовано в Deal»), инструкция пользователя
|
||||
`docs/user-guide/Инструкция-пользователя-Дейл.md`.
|
||||
|
||||
## Manual-чек-лист (остаток после live-приёмок)
|
||||
|
||||
Выполнено живьём на Docker (автономно от авто-прогонов Task 16, build/test/config/syntax — там же):
|
||||
|
||||
1. ✅ **SaaS-сквозная приёмка (live, 15/15 PASS)** — `run-live-saas.sh` + `live-saas-check.sh` на поднятом
|
||||
dev-Postgres (:5433, миграции SystemSaaS + SessionsImpersonationMark применены): оператор login →
|
||||
создать тенанта → инвайт → `POST /api/join` → вход пользователя → settings/cards (на тот момент —
|
||||
`boards`/демо-карточка) →
|
||||
IDOR-негатив 401 (пользователь к операторским ручкам) → suspend (вход 403) → resume (вход 200) →
|
||||
лимиты (tenant_limits) → аудит-лента. Core погашен, :5080 свободен.
|
||||
2. ✅ **dev-smoke 14/14 PASS** — полный gRPC-стек (`scripts/dev-smoke.sh`): подъём, health, login
|
||||
admin/admin, `/api/tg/status` idle, `POST /api/cards` (карточка `planned`) → trash → обучающий сигнал spam,
|
||||
ML-флашер выгрузил outbox. Стек погашен скриптом (trap).
|
||||
3. ✅ **Prod-контур + mTLS + observability (live)** — сертификаты перегенерированы (`scripts/mtls-certs.sh -f`,)
|
||||
полный набор в deploy/certs; подъём `compose.prod.yml` + `--profile observability` с фиктивными
|
||||
env-секретами (`DEAL_MTLS_ENABLED=1`, endpoint'ы https://): core/telegram/ai/ml **healthy** под mTLS;
|
||||
исходящее mTLS подтверждено живьём — `/api/tg/status` (idle) и `/api/ml/status` (reachable:true) через
|
||||
Caddy; фронт и `/api/health` через Caddy 200; promtail→loki (логи пишутся), Grafana 200. Исправлен
|
||||
дефект `deploy/observability/loki.yml` (Loki 3.x: `delete_request_store`). `.env.prod` тестовый удалён.
|
||||
4. ✅ **backup/restore (live, на копии)** — `backup.sh`: pg (-Fc) + minio (docker-mc) + data (tar docker-томов)
|
||||
+ retention; `restore.sh pg` в копию-БД — 43 таблицы/3 схемы идентичны, данные сошлись (users=2,
|
||||
tenants=2, sessions=30); `restore.sh minio` с реальным объектом (залит→бэкап→удалён→восстановлен);
|
||||
`restore.sh data`. Исправлены дефекты скриптов, проявившиеся живьём: двойная схема в MC_HOST_deal
|
||||
(`deal-backup-lib.sh`), пустой бакет → mv (`backup.sh`), Windows/MSYS docker-пути (`host_docker_path`).
|
||||
5. ❌ Реальный Telegram-вход (api_id/api_hash/QR) и LLM-вызовы — **нужны живые креды**.
|
||||
6. ✅ Прогон `scripts/backup.sh` и restore-тест — см. п.4 (полный цикл на dev-хранилищах и копии-БД).
|
||||
7. ✅ **Этап 10 — runtime-приёмка (Docker dev-стек)** — миграция `AddTokenUsageEvents` применена к
|
||||
dev-Postgres (`:5433`), `deal-core` пересобран/healthy; операторский вход `operator`/`operator` → 200;
|
||||
`GET /api/operator/tenants`, `/audit`, `/analytics/overview`, `/analytics/tokens?groupBy=day|provider`,
|
||||
`/analytics/activity?limit=3` → 200 (реальные лента/агрегаты); фронт `npm run build` зелёный, dev-сервер
|
||||
отдаёт `#/operator` и `#/join`; core-тесты 1173/1173. Детали — ledger этапа 10.
|
||||
|
||||
## Заделы (этап 13+; подробно — техдок §11 и roadmap)
|
||||
|
||||
> **Единый источник отложенного и техдолга — `backlog.md` в корне.** Ниже — краткая выжимка.
|
||||
|
||||
- **Этап 11 — Локализация интерфейса (i18n)** — **выполнен** (2026-09-10, урезанный объём): все
|
||||
пользовательские строки фронта в ресурсах (`src/frontend/src/i18n/`, 1039 ключей в 13 областях),
|
||||
линтер `npm run lint:i18n`. Переключатель языка и второй язык — **в бэклоге**: делаем, когда появится
|
||||
потребность (ядро i18n/`registerLocale` к этому готово).
|
||||
- **Этап 12 — Наблюдаемость/устойчивость/перф** — **выполнен** (2026-09-10, автономно, пакеты A–D):
|
||||
метрики Prometheus+Grafana (`/metrics` :9464 во всех процессах); распределённый rate-limit и
|
||||
`LoginAttemptGuard` на Postgres; мгновенный разлогин suspended-сессий; авто-purge `audit_log`/`tenant_limits`;
|
||||
разбиение бандла фронта + прогрессивный рендер колонок; LRU-кэши WTelegram; пакетная миграция схем тенантов;
|
||||
реальный `reclassify` с Local-фолбэком. LEDGER: `.superpowers/sdd/deal-stage12-observability-hardening/`.
|
||||
- **Остатки этапа 12 (закрыто 2026-09-10):** доки под этап 12 (real-reclassify, maintenance-migrate, без
|
||||
демо), Prometheus alert rules (`deploy/observability/prometheus-rules.yml` + провижининг), устранена гонка
|
||||
`FreeTcpPort()` в тест-харнессе (единый `TestPort`), SSE `cards_reclassified` (бэк+фронт), нагрузочные
|
||||
скрипты `scripts/loadtest/`, скан уязвимостей — **чисто** (core: 0 уязвимых пакетов; frontend `npm audit`: 0).
|
||||
- Прочее (требует владельца/кредов): биллинг/провайдер планов и саморегистрация; мультиаккаунтность Telegram;
|
||||
k8s/Cloudflare; Kafka; экспорт/импорт ML; переключатель языка/второй язык (в бэклоге — по потребности);
|
||||
реальный Telegram-вход и живые LLM-вызовы;
|
||||
legacy-прототип `docker-compose.yml` перенесён в `archive/leadradar-legacy/` (2026-09-10).
|
||||
- **Добивка по ТЗ (2026-09-10, автономно)** — закрыты найденные аудитом частично/незакрытые пункты:
|
||||
ML-проверка на канале/сообщении (`/api/ml/candidates`+`/apply` — реальные, не заглушки); глобальные
|
||||
исключения до ML/ИИ (§5.14); новые группы фильтров колонки `levels/locations/types/prices` (§6.3);
|
||||
«открыть исходник» как быстрое действие на карточке (§6.6); глубины очередей/сессии в `/api/operator/health` (§10.2);
|
||||
детектор подозрительной активности `/api/operator/analytics/suspicious` (§10.5); раздел настроек и
|
||||
поддержка тем «Внешний вид» — тёмная (дефолт) / светлая / системная (§8.12). Отчёт аудита:
|
||||
`docs/superpowers/reviews/2026-09-10-tz-compliance-audit.md`. Core-тесты **1275/1275**; фронт build+`lint:i18n` зелёные.
|
||||
- **Telegram-ключи — вариант A (2026-09-10, по решению владельца):** `api_id`/`api_hash` задаёт
|
||||
**оператор глобально** (таблица `public.global_settings`, ручки `GET/PUT /api/operator/settings/telegram-keys`,
|
||||
hash шифруется; раздел «Telegram» в оператор-консоли). У тенанта ключи убраны — только подключение
|
||||
аккаунта; без ключей подключение недоступно (`/api/tg/status → keysSet:false`). Миграция `GlobalSettings`
|
||||
**применена**; `PUT` поддерживает частичное обновление (можно сменить одно поле). Core-тесты **1275/1275**.
|
||||
|
||||
## Code-quality rework (2026-09-08, после ревью 5 зон ~1100 файлов)
|
||||
|
||||
Проведено многоосевое ревью (безопасность/корректность/архитектура/перф) бэкенда и фронта; findings —
|
||||
`docs/superpowers/reviews/2026-09-08-code-quality-review.md`. Все исправления закрыты и проверены:
|
||||
**core 1135/1135, telegram 118/118, ai 52/52, ml 38/38**, фронт build OK, 4 sln 0/0. Главное:
|
||||
|
||||
- **Безопасность**: SSRF-гейт (baseUrl каталоговых провайдеров фиксирован + private-IP-блок в проверке),
|
||||
fail-closed Production (rate limit/CORS/conn-string обязательны), код инвайта в аудите — SHA-256, пароль ≥8,
|
||||
маска ключа не перезаписывает ключ и короткие секреты скрыты, TenantId = 32-hex, mTLS fail-closed,
|
||||
gRPC-лимиты входных данных, join проверяет целевого тенанта, атомарный инкремент токенов (Npgsql).
|
||||
- **Корректность**: атомарные append комментариев/ссылок/файлов (1 SQL), уникальный objectKey, атомарный
|
||||
дедуп-pump, запрет move из trash/archive/taken, логирование «немых» catch, очистка сессий вне hot-path;
|
||||
фронт: смена пароля с текущим паролем, boot не падает от одного 500, автосейв не затирает промпты,
|
||||
гонки поиска закрыты seq-токенами.
|
||||
- **Архитектура**: `Deal.Grpc.Hosting` (общая обвязка 3 сервисов), `TenantSettingsSnapshot` (9 копий чтения
|
||||
настроек → одна), декомпозиция 7 крупных файлов на partial (<350 строк), фронт store.js → слайсы `store/`,
|
||||
вынесены компоненты Settings/Discovery; мёртвый код удалён.
|
||||
- **Реестры констант (C35, 2026-09-09)**: общие `Deal.Contracts.Integrations.MlLearningLabels`
|
||||
(spam/t:hire/t:order) и `SourceDefaults` (DefaultHue) вместо дублей в 5 модулях; единый предикат «активные
|
||||
правила» (Kanban `ColumnRules.HasActiveRules`); реестр id-стадий `ProjectStages` (с этапа 9 — `CardsDefaultContainers`); общий
|
||||
`CardsService.JustNowLabel`; TTL-эвикция в DiscoverySearchErrorCounter (+4 теста, core 1139).
|
||||
- **Заделы** (не рисковали без e2e/не успели): вынос оставшихся вкладок SettingsView, Optional-пункты
|
||||
(пагинация колонок, виртуализация, LRU-кэши WTelegram и др.). Подробности — в отчёте ревью и
|
||||
`.superpowers/sdd/deal-stage8-quality-rework/`.
|
||||
|
||||
## Процесс (обязательства, чтобы не жрать память/хосты)
|
||||
|
||||
- Acceptance-серверы — только через враппер с гарантированным kill (taskkill //T //F по PID-файлу) +
|
||||
проверка освобождения порта; в конце каждой приёмки — шаг очистки.
|
||||
- Контейнеры — по требованию (`docker compose ... up|down`); между заходами ничего не держать;
|
||||
`scripts/cleanup-dev.sh` (kill висящих Deal.*/тест-хостов, `dotnet build-server shutdown`,
|
||||
остановка deal-контейнеров) — в конце захода.
|
||||
- **Хвостов не оставлять (правило владельца, 2026-09-10):** по завершении работы все сервисы и процессы
|
||||
должны быть остановлены — включая Docker-контейнеры и dev-серверы (frontend/Vite, dotnet) — если
|
||||
владелец явно не попросил оставить их запущенными. Проверка в конце: `docker ps` без `deal-*`,
|
||||
свободные порты (5173/5080/5082/5101/5102/5103/5433/9000/9001/9464), `dotnet build-server shutdown`.
|
||||
- **Разовые решения — одним списком в начале (правило владельца, 2026-09-10):** все вопросы, требующие
|
||||
выбора владельца, собираются и задаются **сразу, до начала работы**, а не по ходу/в конце. **Не
|
||||
спрашивать о том, что уже определено ТЗ/принятыми решениями** — это делать без вопросов; вопрос —
|
||||
только при реальном противоречии в требованиях. При неоднозначности без противоречий — выбирать
|
||||
безопасный обратимый дефолт и делать (напр. перенос, а не удаление).
|
||||
- **Легаси-прототип LeadRadar** перенесён из корня в `archive/leadradar-legacy/` (2026-09-10; обратимо,
|
||||
на сборку/запуск не влияет).
|
||||
|
||||
@@ -1,341 +1,341 @@
|
||||
# Поиск и подключение каналов (Discovery) — Implementation Plan
|
||||
|
||||
> Исторический документ (план Discovery прототипа LeadRadar, 2026-09-04). Актуальное состояние — `docs/superpowers/STATUS.md` и `docs/technical/Техническая-документация-Дейл.md`.
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Дать пользователю возможность создавать «задачи поиска»: система находит по описанию/ключам Telegram-каналы и группы (в которых мы не состоим), оценивает их (метаданные → язык → контент по темам), показывает «на рассмотрение», а человек вступает сам или включает авто-вступление в рамках суточных квот с анти-бан паузами.
|
||||
|
||||
**Architecture:** Дочерняя система Discovery поверх существующего стека (FastAPI + DuckDB + TelegramManager/Telethon + Vue 3). Отдельный сервис `discovery` (хранилище+оркестрация), новые методы Telegram-действий в `TelegramManager`, общий BanGuard для квот/пауз, отдельный фоновый воркер в `main.py`. Оценка сообщений переиспользует правила/ML/ИИ, но с профилем задачи и БЕЗ создания карточек. UI — подвкладка «Поиск» на экране «Каналы».
|
||||
|
||||
**Tech Stack:** Python 3.12 / FastAPI / DuckDB / Telethon / Vue 3 + Tailwind (Vite). Новых зависимостей нет.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Правило «мы не состоим» — глобальное и безусловное: источники из `dialogs`, чёрного списка или уже в другой задаче исключаются сразу (проверка повторяется и при вступлении).
|
||||
- Спека: `docs/superpowers/specs/2026-09-04-channel-discovery-design.md` (читать при каждом задании).
|
||||
- Проект НЕ git-репозиторий: вместо `git commit` — проверка через `docker compose`/`npm run build`, фиксация результата в тексте шага.
|
||||
- Все тексты UI — по-русски, в стиле существующего интерфейса (без канцелярита, короткие подписи).
|
||||
- Все настраиваемые числа (лимиты, паузы, пороги, размеры выборок) — настройки в БД (`store.get_setting`), НЕ в коде; дефолты в `constants.DEFAULT_SETTINGS`.
|
||||
- Новые таблицы добавлять только через `db.py` (`_SCHEMA`, `CREATE TABLE IF NOT EXISTS`), при необходимости — миграции в `_MIGRATIONS`.
|
||||
- Запуск/проверка: контейнеры `docker compose up -d`, бэкенд на :8000, ML на :8100; пересборка `docker compose build app`.
|
||||
- JSON-поля (keywords/marks/topics) хранить как VARCHAR с `json.dumps(..., ensure_ascii=False)`, читать через `json.loads` — как в остальном коде.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Схема БД и настройки по умолчанию
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/db.py` (добавить 4 таблицы в `_SCHEMA`)
|
||||
- Modify: `backend/app/constants.py` (`DEFAULT_SETTINGS`)
|
||||
- Modify: `backend/app/routers/settings_routes.py` (`_PUBLIC_INT`)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: таблицы `disc_tasks`, `disc_candidates`, `disc_blacklist`, `disc_log`; настройки `discJoinLimit` (50), `discJoinDelayMin` (50), `discJoinDelayMax` (70), `discEvalSample` (10), `discEvalThreshold` (40).
|
||||
|
||||
- [ ] **Step 1: Добавить таблицы в `_SCHEMA`** (перед таблицей `settings`)
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS disc_tasks (
|
||||
id VARCHAR PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
description VARCHAR NOT NULL DEFAULT '',
|
||||
keywords VARCHAR NOT NULL DEFAULT '[]',
|
||||
min_subscribers INTEGER NOT NULL DEFAULT 0,
|
||||
lang VARCHAR NOT NULL DEFAULT 'ru',
|
||||
threshold INTEGER NOT NULL DEFAULT 40,
|
||||
sample_size INTEGER NOT NULL DEFAULT 10,
|
||||
plan_joins INTEGER NOT NULL DEFAULT 1,
|
||||
auto_join BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
status VARCHAR NOT NULL DEFAULT 'draft', -- draft|running|paused|done|failed
|
||||
search_idx INTEGER NOT NULL DEFAULT 0,
|
||||
search_done BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
found INTEGER NOT NULL DEFAULT 0,
|
||||
evaluated INTEGER NOT NULL DEFAULT 0,
|
||||
joined INTEGER NOT NULL DEFAULT 0,
|
||||
rejected INTEGER NOT NULL DEFAULT 0,
|
||||
created_at BIGINT NOT NULL,
|
||||
updated_at BIGINT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS disc_candidates (
|
||||
dialog_id VARCHAR PRIMARY KEY,
|
||||
task_id VARCHAR NOT NULL,
|
||||
name VARCHAR NOT NULL DEFAULT '',
|
||||
username VARCHAR NOT NULL DEFAULT '',
|
||||
kind VARCHAR NOT NULL DEFAULT 'channel', -- channel|group|forum
|
||||
hue VARCHAR NOT NULL DEFAULT '#666',
|
||||
participants INTEGER,
|
||||
lang_ru BOOLEAN,
|
||||
marks VARCHAR NOT NULL DEFAULT '[]',
|
||||
topics VARCHAR NOT NULL DEFAULT '[]',
|
||||
fit_ratio DOUBLE,
|
||||
status VARCHAR NOT NULL DEFAULT 'new', -- new|review|joined|rejected
|
||||
auto_joined BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at BIGINT NOT NULL,
|
||||
updated_at BIGINT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_disc_cand_task ON disc_candidates(task_id, status);
|
||||
CREATE TABLE IF NOT EXISTS disc_blacklist (
|
||||
dialog_id VARCHAR PRIMARY KEY,
|
||||
name VARCHAR NOT NULL DEFAULT '',
|
||||
reason VARCHAR NOT NULL DEFAULT '',
|
||||
created_at BIGINT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS disc_log (
|
||||
id VARCHAR PRIMARY KEY,
|
||||
task_id VARCHAR NOT NULL,
|
||||
event VARCHAR NOT NULL, -- search|found|skip|eval|review|join_auto|join_manual|leave|reject|flood|error|done
|
||||
text VARCHAR NOT NULL DEFAULT '',
|
||||
created_at BIGINT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_disc_log_task ON disc_log(task_id, created_at);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Добавить настройки в `constants.py` → `DEFAULT_SETTINGS`**
|
||||
|
||||
```python
|
||||
# поиск каналов (Discovery)
|
||||
"discJoinLimit": 50, # суточный лимит авто-вступлений (общий)
|
||||
"discJoinDelayMin": 50, # сек, нижняя граница паузы между авто-вступлениями
|
||||
"discJoinDelayMax": 70, # сек, верхняя граница
|
||||
"discEvalSample": 10, # размер выборки сообщений при оценке
|
||||
"discEvalThreshold": 40, # % подходящих сообщений
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Открыть настройки наружу в `settings_routes.py`**
|
||||
|
||||
В `_PUBLIC_INT` добавить `discJoinLimit, discJoinDelayMin, discJoinDelayMax, discEvalSample, discEvalThreshold`. В `patch_settings` наложить границы: limit 1..200; паузы 5..600; sample 3..30; threshold 1..100.
|
||||
|
||||
- [ ] **Step 4: Проверить**
|
||||
|
||||
```bash
|
||||
docker compose build app && docker compose up -d app
|
||||
```
|
||||
Затем `GET /api/settings` (после логина) — в ответе присутствуют `discJoinLimit: 50` и остальные ключи. `python -m py_compile` всех изменённых файлов — без ошибок.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: BanGuard (квоты, паузы, flood)
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/services/ban_guard.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `store`, настройки из Task 1.
|
||||
- Produces:
|
||||
- `def joins_today_auto() -> int` — авто-вступления за текущие UTC-сутки (считает `disc_log` event='join_auto', `created_at >= начало суток`).
|
||||
- `def can_auto_join() -> bool` — лимит не исчерпан И нет flood-блокировки на сегодня И нет глобальной паузы.
|
||||
- `async def wait_join_delay() -> None` — `asyncio.sleep(random.uniform(min, max))`.
|
||||
- `def note_flood() -> None` — `store.set_setting("discFloodDay", <start_of_day_ms>)`.
|
||||
- `def flood_today() -> bool`
|
||||
- `def global_paused() -> bool` / `def set_global_pause(v: bool) -> None` (setting `discPaused`)
|
||||
- `def search_pause() -> float` — `random.uniform(2.0, 4.0)`.
|
||||
|
||||
- [ ] **Step 1: Реализовать модуль** (~60 строк; начало суток — UTC: `datetime.now(timezone.utc).replace(hour=0,minute=0,second=0,microsecond=0)` → ms).
|
||||
|
||||
- [ ] **Step 2: Проверить на временной БД в контейнере**
|
||||
|
||||
```bash
|
||||
docker compose run --rm --no-deps -e PYTHONPATH=/srv -e LEADRADAR_DATA=/tmp/lr_bg --entrypoint python app -c "
|
||||
from app.db import store; store.init()
|
||||
from app.services import ban_guard as bg
|
||||
assert bg.can_auto_join() is True
|
||||
assert bg.joins_today_auto() == 0
|
||||
bg.note_flood(); assert bg.flood_today() is True
|
||||
bg.set_global_pause(True); assert bg.can_auto_join() is False
|
||||
print('BANGUARD OK')
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Хранилище Discovery (задачи/кандидаты/чёрный список/лог)
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/services/discovery.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `store` (таблицы Task 1).
|
||||
- Produces (все синхронные):
|
||||
- `list_tasks() -> list[dict]`, `get_task(id) -> dict | None` (keywords — список)
|
||||
- `create_task(payload: dict) -> dict` — валидация: name непустое; `plan_joins` 1..limit; **правило бюджета**: `sum(plan_joins задач, где status NOT IN ('done','failed')) + plan_joins <= discJoinLimit`, иначе `raise ValueError(...)`.
|
||||
- `patch_task(id, patch: dict) -> dict` (name/description/keywords/min_subscribers/lang/threshold/sample_size/plan_joins/auto_join; увеличение plan_joins — с той же проверкой)
|
||||
- `delete_task(id) -> None` (удалить задачу и её кандидатов)
|
||||
- `start_task(id) -> dict` — требует непустой keywords; status=running; `pause_task(id) -> dict` — paused
|
||||
- `list_candidates(task_id, status: str | None) -> list[dict]` (декод marks/topics)
|
||||
- `add_candidate(task_id, dialog_id, name, username, kind, hue) -> dict | None` — `None`, если: в `dialogs`, в `disc_blacklist`, либо уже есть `disc_candidates` со статусом new/review/joined. Лог `skip` с причиной.
|
||||
- `bump_counter(task_id, field: str, n: int = 1)` — found/evaluated/joined/rejected
|
||||
- `set_candidate(task_id, dialog_id, patch: dict)` — обновление полей кандидата
|
||||
- `set_candidate_status(dialog_id, status)` + лог
|
||||
- `delete_candidate(dialog_id) -> None` — удалить кандидата (skip-ветки)
|
||||
- `advance_search(task_id) -> None` — `search_idx += 1`; когда индекс >= len(keywords) → `search_done=True`
|
||||
- `mark_joined(dialog_id, auto: bool)` — статус joined + `bump_counter('joined')` + лог `join_auto`/`join_manual`
|
||||
- `mark_rejected(dialog_id, reason="")` — статус rejected + `bump_counter('rejected')` + лог `reject` + `add_blacklist`
|
||||
- `add_blacklist(dialog_id, name, reason) / remove_blacklist(dialog_id) / list_blacklist() -> list[dict]`
|
||||
- `add_log(task_id, event, text="")`; `task_log(task_id, limit=100) -> list[dict]`
|
||||
|
||||
- [ ] **Step 1: Реализовать модуль** (json-поля по конвенции проекта; все `store.execute/query` с параметрами).
|
||||
- [ ] **Step 2: Проверить на временной БД** (сценарий): создать задачу plan=25 → ок; создать вторую plan=30 → `ValueError`; кандидат, совпадающий с `dialogs` → `add_candidate` вернул None + лог skip; `mark_rejected` → в чёрном списке; повторный `add_candidate` того же источника → None.
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Telegram-действия поиска (методы TelegramManager)
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/services/telegram.py` (класс `TelegramManager`)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `self.client`, `ban_guard`.
|
||||
- Produces (async-методы):
|
||||
- `async def discovery_search(q: str, limit: int = 30) -> list[dict]` — `client(functions.contacts.SearchRequest(q=q, limit=limit))`; вернуть `[{id(str), name, username, kind, hue}]` (kind через `_kind_of`, hue через `dialog_hue`); между вызовами — `await asyncio.sleep(ban_guard.search_pause())`.
|
||||
- `async def discovery_info(dialog_id: str) -> dict` — `{id, name, username, kind, hue, participants: int | None, is_forum: bool}` (participants из `full_chat` где возможно; иначе None).
|
||||
- `async def discovery_read(dialog_id: str, limit: int) -> dict` — последние сообщения: `{"ok": bool, "error": str | None, "messages": [{"id", "text", "date_ms", "topic_id"}]}`; `topic_id` — `getattr(getattr(m,'reply_to',None),'reply_to_top_id',None)`. История недоступна → `{"ok": False, "error": "no_history", "messages": []}`.
|
||||
- `async def discovery_join(username: str) -> None` — `client(functions.channels.JoinChannelRequest(...))`; ПЕРЕД вызовом `await ban_guard.wait_join_delay()`; `FloodWaitError` → `ban_guard.note_flood()` и проброс.
|
||||
- `async def discovery_leave(dialog_id: str) -> None` — `channels.LeaveChannelRequest`.
|
||||
- `def add_dialog_monitored(dialog_id, name, username, kind, hue) -> None` — INSERT/UPDATE `dialogs` с `monitor=TRUE, backfilled=FALSE` (как в `set_monitor`, но без авто-join-логики).
|
||||
|
||||
- [ ] **Step 1: Реализовать методы** (импорт `telethon.tl.functions`, `telethon.errors.rpcerrorlist.FloodWaitError`).
|
||||
- [ ] **Step 2: Проверить компиляцию** `py_compile`. Полная проверка — после Task 6/7 на живом аккаунте (ручные шаги в Task 10).
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Оценка контента (язык, темы, fit по профилю задачи)
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/services/discovery_eval.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `store`, `ml_client`, `ai_service` (chat_json), `pipeline.clean_short`.
|
||||
- Produces:
|
||||
- `def detect_lang_ru(texts: list[str]) -> bool | None` — доля кириллических букв от всех букв в сумме: `>=0.15 → True`; `<=0.03 → False`; между порогами → `None` (неопределённо).
|
||||
- `def group_by_topic(messages: list[dict]) -> list[dict]` — группировка по `topic_id` (None → "main"); возвращает `[{"topic_id", "title", "messages": [...]}]`, title = сниппет первого текста темы (≤60 симв.), сортировка по количеству сообщений (убыв.).
|
||||
- `async def evaluate_message(task: dict, text: str) -> dict` — `{"fit": bool, "reason": str, "source": "heuristic"|"ml"|"ai"}`:
|
||||
1) текст пустой/длина <10 → fit False «слишком короткое»;
|
||||
2) ML: если `ml_client.is_enabled()` и прогноз `take` и `label=='spam'` → fit False «ML: спам»;
|
||||
3) ИИ: если `aiEnabled` → один JSON-вызов `ai_service.chat_json(промпт, user=text)` с промптом из описания задачи и ключей (`{fit, reason}`); ошибка → шаг 4;
|
||||
4) эвристика: fit = любой ключ входит в `clean_short(text)` casefold; reason «совпал ключ "…"» / «нет совпадений с ключами».
|
||||
- `async def evaluate_sample(task: dict, messages: list[dict]) -> dict` — последовательно по каждому сообщению; вернуть `{"fit_count": int, "total": int, "fit_ratio": float, "per_message": [{"text": …, "fit", "reason", "topic_id"}]}`.
|
||||
- `def passed(ev: dict, task: dict) -> bool` — `ev["total"] >= 3 and ev["fit_ratio"]*100 >= task["threshold"]`.
|
||||
|
||||
- [ ] **Step 1: Реализовать модуль**. Промпт ИИ (внутри модуля, константа):
|
||||
`Оцени, относится ли сообщение к сфере/задаче. Описание: {description}. Ключи: {keywords}. Верни JSON {"fit": 0|1, "reason": "краткая причина"}.`
|
||||
|
||||
- [ ] **Step 2: Проверить на временной БД** (без сети): `detect_lang_ru(["Ищем python разработчика"]) is True`; `detect_lang_ru(["we need a python developer"]) is False`; `group_by_topic` объединяет по topic_id и сортирует; `evaluate_message` на задаче без ИИ/ML возвращает эвристический fit по ключу.
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Воркер Discovery (поиск → оценка → авто-вступление)
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/services/discovery_worker.py`
|
||||
- Modify: `backend/app/main.py` (фоновый цикл `_discovery_loop`, каждые 5 c)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `discovery` (Task 3), `tg.discovery_*` (Task 4), `discovery_eval` (Task 5), `ban_guard` (Task 2).
|
||||
- Produces: `async def tick() -> dict` — выполняет ОДНО действие и возвращает `{"action": ..., "taskId": ...}` (или `{"action": "none"}`).
|
||||
|
||||
Логика tick (по одной задаче за вызов, начиная с самой старой running):
|
||||
1. Если задача `search_done=False`: взять ключ `keywords[search_idx]`, вызвать `tg.discovery_search`; для каждого результата `discovery.add_candidate`; `discovery.advance_search(task_id)`; если `search_done` стал True — лог `search` «поиск завершён: N кандидатов». Возврат.
|
||||
2. Иначе взять первого кандидата статуса `new` задачи:
|
||||
- `info = tg.discovery_info`; `participants`, `kind` (forum если `is_forum`); при заданном `min_subscribers` и participants НЕ None и меньше минимума — `set_candidate_status(...)` нет: просто `discovery.delete_candidate` + лог `skip`; если participants None — метка «участники не подтверждены» (идём дальше).
|
||||
- `read = tg.discovery_read(dialog_id, sample_size)`.
|
||||
- Если `read.ok=False` (история недоступна без членства): kind==channel → `review` с меткой «канал: история недоступна»; группа/форум → `review` с меткой «закрытая группа (история скрыта) — вступите сами»; оценка контента не производится, неподтверждённые фильтры помечаются.
|
||||
- Язык: если прочитано и `task.lang=='ru'`: `lang_ru=detect_lang_ru(...)`; False → удалить кандидата, лог `skip` «язык не русский»; None → метка «язык не подтверждён».
|
||||
- Оценка: `evaluate_sample`; `passed` → метки topics/fit → `review` + лог `review`; иначе удалить кандидата, лог `skip` «мало подходящих (X из N)».
|
||||
3. Авто-вступление (отдельный проход tick, приоритет ниже оценки): если у running-задачи `auto_join` и есть кандидат `review` и `ban_guard.can_auto_join()`:
|
||||
- повторная проверка «мы не состоим» (`dialogs`/blacklist) → если вступили уже → `mark_rejected` с логом;
|
||||
- `await ban_guard.wait_join_delay()` (рандом 50–70 с — спейсинг авто-вступлений; ручные join из API паузу не делают);
|
||||
- `tg.discovery_join(username)` → `discovery.mark_joined(dialog_id, auto=True)` → `tg.add_dialog_monitored(...)`; при FloodWaitError → `ban_guard.note_flood()` + лог `flood`.
|
||||
4. Если `task.joined >= task.plan_joins` → статус `done`, лог `done`.
|
||||
|
||||
- [ ] **Step 1: Реализовать** `discovery_worker.py` и цикл в `main.py`.
|
||||
- [ ] **Step 2: Проверить компиляцию** и запуск без падений (воркер с пустыми таблицами делает `none`). Полный прогон — Task 10 вручную.
|
||||
|
||||
---
|
||||
|
||||
### Task 7: API Discovery
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/routers/discovery_routes.py`
|
||||
- Modify: `backend/app/main.py` (регистрация роутера)
|
||||
|
||||
**Interfaces:**
|
||||
- Prefix `/api/discovery`, auth `current_login`:
|
||||
- `GET /tasks`, `POST /tasks`, `PATCH /tasks/{id}`, `DELETE /tasks/{id}`, `POST /tasks/{id}/start`, `POST /tasks/{id}/pause`
|
||||
- `POST /tasks/{id}/generate-keywords` — ИИ: промпт по description → JSON `{"keywords": [...]}` (8–16 строк RU+EN); ИИ недоступен/выключен → `{"keywords": [], "error": "..."}`.
|
||||
- `GET /tasks/{id}/candidates?status=`
|
||||
- `POST /candidates/{dialog_id}/join` — ручное вступление (вне квот): `tg.discovery_join` + `add_dialog_monitored` + `mark_joined(auto=False)`; 400 при ошибке.
|
||||
- `POST /candidates/{dialog_id}/reject` — `mark_rejected` (добавляет в чёрный список). Если кандидат уже `joined` — 400.
|
||||
- `GET /blacklist`, `DELETE /blacklist/{dialog_id}`
|
||||
- `GET /tasks/{id}/log`
|
||||
|
||||
Pydantic-модели: `TaskCreate` (name, description, keywords, minSubscribers, lang, threshold, sampleSize, planJoins, autoJoin), `TaskPatch` (все optional), `GenKeywordsBody` не нужен (id в пути).
|
||||
|
||||
- [ ] **Step 1: Реализовать роутер** (ValueError → HTTPException 400; KeyError → 404).
|
||||
- [ ] **Step 2: Зарегистрировать в main.py**.
|
||||
- [ ] **Step 3: Проверить API на живом контейнере**: логин, создание задачи plan=1, list, delete; `generate-keywords` вернёт error-ветку без настроенного ИИ (не падает).
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Фронтенд — store + каркас подвкладки «Поиск»
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/store.js`
|
||||
- Create: `frontend/src/views/DiscoveryView.vue`
|
||||
- Modify: `frontend/src/views/ChannelsView.vue`
|
||||
|
||||
**Interfaces:**
|
||||
- state: `channelsTab: 'list' | 'search'`, `discTasks: []`, `discCandidates: []`, `discBlacklist: []`, `discLog: []`, `discActiveTaskId: null`, `discCandidateStatus: 'review'`, `discBusy: false`.
|
||||
- store-функции: `gotoChannelsTab(tab)`, `loadDiscTasks()`, `saveDiscTask(form, id=null)` (create/patch), `deleteDiscTask(id)`, `startDiscTask(id)`, `pauseDiscTask(id)`, `generateDiscKeywords(taskId)`, `loadDiscCandidates(taskId, status)`, `joinDiscCandidate(c)`, `rejectDiscCandidate(c)`, `loadDiscBlacklist()`, `removeDiscBlacklist(id)`, `loadDiscLog(taskId)`.
|
||||
|
||||
- [ ] **Step 1: store.js** — состояние + функции (паттерны: `api.get/post/patch/delete`, `toast`, `errMsg`).
|
||||
- [ ] **Step 2: ChannelsView.vue** — в шапке сегмент: «Каналы | Поиск» (`state.channelsTab`), содержимое по табу.
|
||||
- [ ] **Step 3: DiscoveryView.vue (каркас)**: левая колонка — список задач (+ «Новая задача»); правая — панель задачи: мастер (name, description, «Сгенерировать ключи ИИ», чипы ключей редактируемые, minSubscribers, lang select ru/any, threshold, sampleSize, planJoins, autoJoin toggle, кнопки «Запустить/Пауза/Удалить»).
|
||||
- [ ] **Step 4: `npm run build`** — без ошибок.
|
||||
|
||||
---
|
||||
|
||||
### Task 9: Фронтенд — кандидаты, действия, чёрный список, история
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/views/DiscoveryView.vue`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 8 (store).
|
||||
|
||||
- [ ] **Step 1: Табы панели задачи**: «В обработке» (`new`) / «На рассмотрении» (`review`) / «Вступили» (`joined`) / «Отклонены» (`rejected`) + «История» (лог). Бейджи счётчиков скрыты при 0.
|
||||
- [ ] **Step 2: Карточка кандидата**: название, @username, kind-иконка/метка (канал/группа/форум), метки marks (чипы: закрытая, не прочитан, участники/язык не подтверждены, мало сообщений), участники, «подходит X из N», кнопки «Вступить и мониторить» / «Отклонить» (только для review). Форум → раскрывающийся список topics («тема — подходит X из N»).
|
||||
- [ ] **Step 3: Чёрный список** (под списками или отдельный таб) — снять источник; «Настройки квот» — popover/inline с `discJoinLimit/discJoinDelayMin/discJoinDelayMax` + стоп-кран (PATCH /api/settings).
|
||||
- [ ] **Step 4: История** — лог задачи.
|
||||
- [ ] **Step 5: `npm run build`** — без ошибок; визуальная проверка основных сценариев (Task 10).
|
||||
|
||||
---
|
||||
|
||||
### Task 10: ТЗ, сборка и end-to-end проверка
|
||||
|
||||
**Files:**
|
||||
- Modify: `ТЗ.md` (раздел «Поиск и подключение каналов»)
|
||||
|
||||
- [ ] **Step 1: Дополнить ТЗ** — кратко: задачи поиска, каскад фильтров, глобальное правило «мы не состоим», метки, оценка по темам, review/join/reject + чёрный список, авто-вступление и квоты (50/сутки, 50–70 с), подвкладка «Поиск» на «Каналах».
|
||||
- [ ] **Step 2: Сборка и рестарт**:
|
||||
|
||||
```bash
|
||||
docker compose build app && docker compose up -d app
|
||||
cd frontend && npm run build
|
||||
```
|
||||
|
||||
- [ ] **Step 3: E2E вручную (нужен подключённый Telegram-аккаунт)**:
|
||||
1. «Каналы → Поиск» → создать задачу (описание, план 3, авто-вступление выкл) → «Сгенерировать ключи» → запустить.
|
||||
2. Дождаться кандидатов; проверить, что текущие подписки и отклонённые не появляются.
|
||||
3. Открыть кандидата: метки, участники, fit «X из N», темы форума.
|
||||
4. «Вступить и мониторить» → источник появляется в «Каналах» (monitor on) и начинает давать карточки.
|
||||
5. «Отклонить» → уходит в чёрный список; повторно не находится.
|
||||
6. Включить авто-вступление: проверить паузы (≥50 с) и расход суточного лимита.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- **Покрытие спеки:** Task 1 (хранилище+настройки), Task 2 (квоты/анти-бан), Task 3 (задачи/бюджет планов/чёрный список), Task 4 (поиск/инфо/чтение/join), Task 5 (язык/темы/fit), Task 6 (воркер+авто-join), Task 7 (API), Task 8–9 (UI), Task 10 (ТЗ+E2E). Правило «мы не состоим» — Task 3 `add_candidate`, Task 6 шаг 3 (повторная проверка перед join), UI Task 9. Разделы спеки §4–§12 покрыты; «вне рамок» (§13) не реализуются.
|
||||
- **Плейсхолдеры:** нет; у каждого шага есть конкретный код/поведение и способ проверки.
|
||||
- **Согласованность:** единые статусы задач `draft|running|paused|done|failed`, кандидатов `new|review|joined|rejected`; события лога `search|found|skip|eval|review|join_auto|join_manual|leave|reject|flood|error|done`; все имена настроек и функций совпадают между задачами.
|
||||
# Поиск и подключение каналов (Discovery) — Implementation Plan
|
||||
|
||||
> Исторический документ (план Discovery прототипа LeadRadar, 2026-09-04). Актуальное состояние — `docs/superpowers/STATUS.md` и `docs/technical/Техническая-документация-Дейл.md`.
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Дать пользователю возможность создавать «задачи поиска»: система находит по описанию/ключам Telegram-каналы и группы (в которых мы не состоим), оценивает их (метаданные → язык → контент по темам), показывает «на рассмотрение», а человек вступает сам или включает авто-вступление в рамках суточных квот с анти-бан паузами.
|
||||
|
||||
**Architecture:** Дочерняя система Discovery поверх существующего стека (FastAPI + DuckDB + TelegramManager/Telethon + Vue 3). Отдельный сервис `discovery` (хранилище+оркестрация), новые методы Telegram-действий в `TelegramManager`, общий BanGuard для квот/пауз, отдельный фоновый воркер в `main.py`. Оценка сообщений переиспользует правила/ML/ИИ, но с профилем задачи и БЕЗ создания карточек. UI — подвкладка «Поиск» на экране «Каналы».
|
||||
|
||||
**Tech Stack:** Python 3.12 / FastAPI / DuckDB / Telethon / Vue 3 + Tailwind (Vite). Новых зависимостей нет.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Правило «мы не состоим» — глобальное и безусловное: источники из `dialogs`, чёрного списка или уже в другой задаче исключаются сразу (проверка повторяется и при вступлении).
|
||||
- Спека: `docs/superpowers/specs/2026-09-04-channel-discovery-design.md` (читать при каждом задании).
|
||||
- Проект НЕ git-репозиторий: вместо `git commit` — проверка через `docker compose`/`npm run build`, фиксация результата в тексте шага.
|
||||
- Все тексты UI — по-русски, в стиле существующего интерфейса (без канцелярита, короткие подписи).
|
||||
- Все настраиваемые числа (лимиты, паузы, пороги, размеры выборок) — настройки в БД (`store.get_setting`), НЕ в коде; дефолты в `constants.DEFAULT_SETTINGS`.
|
||||
- Новые таблицы добавлять только через `db.py` (`_SCHEMA`, `CREATE TABLE IF NOT EXISTS`), при необходимости — миграции в `_MIGRATIONS`.
|
||||
- Запуск/проверка: контейнеры `docker compose up -d`, бэкенд на :8000, ML на :8100; пересборка `docker compose build app`.
|
||||
- JSON-поля (keywords/marks/topics) хранить как VARCHAR с `json.dumps(..., ensure_ascii=False)`, читать через `json.loads` — как в остальном коде.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Схема БД и настройки по умолчанию
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/db.py` (добавить 4 таблицы в `_SCHEMA`)
|
||||
- Modify: `backend/app/constants.py` (`DEFAULT_SETTINGS`)
|
||||
- Modify: `backend/app/routers/settings_routes.py` (`_PUBLIC_INT`)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: таблицы `disc_tasks`, `disc_candidates`, `disc_blacklist`, `disc_log`; настройки `discJoinLimit` (50), `discJoinDelayMin` (50), `discJoinDelayMax` (70), `discEvalSample` (10), `discEvalThreshold` (40).
|
||||
|
||||
- [ ] **Step 1: Добавить таблицы в `_SCHEMA`** (перед таблицей `settings`)
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS disc_tasks (
|
||||
id VARCHAR PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
description VARCHAR NOT NULL DEFAULT '',
|
||||
keywords VARCHAR NOT NULL DEFAULT '[]',
|
||||
min_subscribers INTEGER NOT NULL DEFAULT 0,
|
||||
lang VARCHAR NOT NULL DEFAULT 'ru',
|
||||
threshold INTEGER NOT NULL DEFAULT 40,
|
||||
sample_size INTEGER NOT NULL DEFAULT 10,
|
||||
plan_joins INTEGER NOT NULL DEFAULT 1,
|
||||
auto_join BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
status VARCHAR NOT NULL DEFAULT 'draft', -- draft|running|paused|done|failed
|
||||
search_idx INTEGER NOT NULL DEFAULT 0,
|
||||
search_done BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
found INTEGER NOT NULL DEFAULT 0,
|
||||
evaluated INTEGER NOT NULL DEFAULT 0,
|
||||
joined INTEGER NOT NULL DEFAULT 0,
|
||||
rejected INTEGER NOT NULL DEFAULT 0,
|
||||
created_at BIGINT NOT NULL,
|
||||
updated_at BIGINT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS disc_candidates (
|
||||
dialog_id VARCHAR PRIMARY KEY,
|
||||
task_id VARCHAR NOT NULL,
|
||||
name VARCHAR NOT NULL DEFAULT '',
|
||||
username VARCHAR NOT NULL DEFAULT '',
|
||||
kind VARCHAR NOT NULL DEFAULT 'channel', -- channel|group|forum
|
||||
hue VARCHAR NOT NULL DEFAULT '#666',
|
||||
participants INTEGER,
|
||||
lang_ru BOOLEAN,
|
||||
marks VARCHAR NOT NULL DEFAULT '[]',
|
||||
topics VARCHAR NOT NULL DEFAULT '[]',
|
||||
fit_ratio DOUBLE,
|
||||
status VARCHAR NOT NULL DEFAULT 'new', -- new|review|joined|rejected
|
||||
auto_joined BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at BIGINT NOT NULL,
|
||||
updated_at BIGINT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_disc_cand_task ON disc_candidates(task_id, status);
|
||||
CREATE TABLE IF NOT EXISTS disc_blacklist (
|
||||
dialog_id VARCHAR PRIMARY KEY,
|
||||
name VARCHAR NOT NULL DEFAULT '',
|
||||
reason VARCHAR NOT NULL DEFAULT '',
|
||||
created_at BIGINT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS disc_log (
|
||||
id VARCHAR PRIMARY KEY,
|
||||
task_id VARCHAR NOT NULL,
|
||||
event VARCHAR NOT NULL, -- search|found|skip|eval|review|join_auto|join_manual|leave|reject|flood|error|done
|
||||
text VARCHAR NOT NULL DEFAULT '',
|
||||
created_at BIGINT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_disc_log_task ON disc_log(task_id, created_at);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Добавить настройки в `constants.py` → `DEFAULT_SETTINGS`**
|
||||
|
||||
```python
|
||||
# поиск каналов (Discovery)
|
||||
"discJoinLimit": 50, # суточный лимит авто-вступлений (общий)
|
||||
"discJoinDelayMin": 50, # сек, нижняя граница паузы между авто-вступлениями
|
||||
"discJoinDelayMax": 70, # сек, верхняя граница
|
||||
"discEvalSample": 10, # размер выборки сообщений при оценке
|
||||
"discEvalThreshold": 40, # % подходящих сообщений
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Открыть настройки наружу в `settings_routes.py`**
|
||||
|
||||
В `_PUBLIC_INT` добавить `discJoinLimit, discJoinDelayMin, discJoinDelayMax, discEvalSample, discEvalThreshold`. В `patch_settings` наложить границы: limit 1..200; паузы 5..600; sample 3..30; threshold 1..100.
|
||||
|
||||
- [ ] **Step 4: Проверить**
|
||||
|
||||
```bash
|
||||
docker compose build app && docker compose up -d app
|
||||
```
|
||||
Затем `GET /api/settings` (после логина) — в ответе присутствуют `discJoinLimit: 50` и остальные ключи. `python -m py_compile` всех изменённых файлов — без ошибок.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: BanGuard (квоты, паузы, flood)
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/services/ban_guard.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `store`, настройки из Task 1.
|
||||
- Produces:
|
||||
- `def joins_today_auto() -> int` — авто-вступления за текущие UTC-сутки (считает `disc_log` event='join_auto', `created_at >= начало суток`).
|
||||
- `def can_auto_join() -> bool` — лимит не исчерпан И нет flood-блокировки на сегодня И нет глобальной паузы.
|
||||
- `async def wait_join_delay() -> None` — `asyncio.sleep(random.uniform(min, max))`.
|
||||
- `def note_flood() -> None` — `store.set_setting("discFloodDay", <start_of_day_ms>)`.
|
||||
- `def flood_today() -> bool`
|
||||
- `def global_paused() -> bool` / `def set_global_pause(v: bool) -> None` (setting `discPaused`)
|
||||
- `def search_pause() -> float` — `random.uniform(2.0, 4.0)`.
|
||||
|
||||
- [ ] **Step 1: Реализовать модуль** (~60 строк; начало суток — UTC: `datetime.now(timezone.utc).replace(hour=0,minute=0,second=0,microsecond=0)` → ms).
|
||||
|
||||
- [ ] **Step 2: Проверить на временной БД в контейнере**
|
||||
|
||||
```bash
|
||||
docker compose run --rm --no-deps -e PYTHONPATH=/srv -e LEADRADAR_DATA=/tmp/lr_bg --entrypoint python app -c "
|
||||
from app.db import store; store.init()
|
||||
from app.services import ban_guard as bg
|
||||
assert bg.can_auto_join() is True
|
||||
assert bg.joins_today_auto() == 0
|
||||
bg.note_flood(); assert bg.flood_today() is True
|
||||
bg.set_global_pause(True); assert bg.can_auto_join() is False
|
||||
print('BANGUARD OK')
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Хранилище Discovery (задачи/кандидаты/чёрный список/лог)
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/services/discovery.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `store` (таблицы Task 1).
|
||||
- Produces (все синхронные):
|
||||
- `list_tasks() -> list[dict]`, `get_task(id) -> dict | None` (keywords — список)
|
||||
- `create_task(payload: dict) -> dict` — валидация: name непустое; `plan_joins` 1..limit; **правило бюджета**: `sum(plan_joins задач, где status NOT IN ('done','failed')) + plan_joins <= discJoinLimit`, иначе `raise ValueError(...)`.
|
||||
- `patch_task(id, patch: dict) -> dict` (name/description/keywords/min_subscribers/lang/threshold/sample_size/plan_joins/auto_join; увеличение plan_joins — с той же проверкой)
|
||||
- `delete_task(id) -> None` (удалить задачу и её кандидатов)
|
||||
- `start_task(id) -> dict` — требует непустой keywords; status=running; `pause_task(id) -> dict` — paused
|
||||
- `list_candidates(task_id, status: str | None) -> list[dict]` (декод marks/topics)
|
||||
- `add_candidate(task_id, dialog_id, name, username, kind, hue) -> dict | None` — `None`, если: в `dialogs`, в `disc_blacklist`, либо уже есть `disc_candidates` со статусом new/review/joined. Лог `skip` с причиной.
|
||||
- `bump_counter(task_id, field: str, n: int = 1)` — found/evaluated/joined/rejected
|
||||
- `set_candidate(task_id, dialog_id, patch: dict)` — обновление полей кандидата
|
||||
- `set_candidate_status(dialog_id, status)` + лог
|
||||
- `delete_candidate(dialog_id) -> None` — удалить кандидата (skip-ветки)
|
||||
- `advance_search(task_id) -> None` — `search_idx += 1`; когда индекс >= len(keywords) → `search_done=True`
|
||||
- `mark_joined(dialog_id, auto: bool)` — статус joined + `bump_counter('joined')` + лог `join_auto`/`join_manual`
|
||||
- `mark_rejected(dialog_id, reason="")` — статус rejected + `bump_counter('rejected')` + лог `reject` + `add_blacklist`
|
||||
- `add_blacklist(dialog_id, name, reason) / remove_blacklist(dialog_id) / list_blacklist() -> list[dict]`
|
||||
- `add_log(task_id, event, text="")`; `task_log(task_id, limit=100) -> list[dict]`
|
||||
|
||||
- [ ] **Step 1: Реализовать модуль** (json-поля по конвенции проекта; все `store.execute/query` с параметрами).
|
||||
- [ ] **Step 2: Проверить на временной БД** (сценарий): создать задачу plan=25 → ок; создать вторую plan=30 → `ValueError`; кандидат, совпадающий с `dialogs` → `add_candidate` вернул None + лог skip; `mark_rejected` → в чёрном списке; повторный `add_candidate` того же источника → None.
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Telegram-действия поиска (методы TelegramManager)
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/services/telegram.py` (класс `TelegramManager`)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `self.client`, `ban_guard`.
|
||||
- Produces (async-методы):
|
||||
- `async def discovery_search(q: str, limit: int = 30) -> list[dict]` — `client(functions.contacts.SearchRequest(q=q, limit=limit))`; вернуть `[{id(str), name, username, kind, hue}]` (kind через `_kind_of`, hue через `dialog_hue`); между вызовами — `await asyncio.sleep(ban_guard.search_pause())`.
|
||||
- `async def discovery_info(dialog_id: str) -> dict` — `{id, name, username, kind, hue, participants: int | None, is_forum: bool}` (participants из `full_chat` где возможно; иначе None).
|
||||
- `async def discovery_read(dialog_id: str, limit: int) -> dict` — последние сообщения: `{"ok": bool, "error": str | None, "messages": [{"id", "text", "date_ms", "topic_id"}]}`; `topic_id` — `getattr(getattr(m,'reply_to',None),'reply_to_top_id',None)`. История недоступна → `{"ok": False, "error": "no_history", "messages": []}`.
|
||||
- `async def discovery_join(username: str) -> None` — `client(functions.channels.JoinChannelRequest(...))`; ПЕРЕД вызовом `await ban_guard.wait_join_delay()`; `FloodWaitError` → `ban_guard.note_flood()` и проброс.
|
||||
- `async def discovery_leave(dialog_id: str) -> None` — `channels.LeaveChannelRequest`.
|
||||
- `def add_dialog_monitored(dialog_id, name, username, kind, hue) -> None` — INSERT/UPDATE `dialogs` с `monitor=TRUE, backfilled=FALSE` (как в `set_monitor`, но без авто-join-логики).
|
||||
|
||||
- [ ] **Step 1: Реализовать методы** (импорт `telethon.tl.functions`, `telethon.errors.rpcerrorlist.FloodWaitError`).
|
||||
- [ ] **Step 2: Проверить компиляцию** `py_compile`. Полная проверка — после Task 6/7 на живом аккаунте (ручные шаги в Task 10).
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Оценка контента (язык, темы, fit по профилю задачи)
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/services/discovery_eval.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `store`, `ml_client`, `ai_service` (chat_json), `pipeline.clean_short`.
|
||||
- Produces:
|
||||
- `def detect_lang_ru(texts: list[str]) -> bool | None` — доля кириллических букв от всех букв в сумме: `>=0.15 → True`; `<=0.03 → False`; между порогами → `None` (неопределённо).
|
||||
- `def group_by_topic(messages: list[dict]) -> list[dict]` — группировка по `topic_id` (None → "main"); возвращает `[{"topic_id", "title", "messages": [...]}]`, title = сниппет первого текста темы (≤60 симв.), сортировка по количеству сообщений (убыв.).
|
||||
- `async def evaluate_message(task: dict, text: str) -> dict` — `{"fit": bool, "reason": str, "source": "heuristic"|"ml"|"ai"}`:
|
||||
1) текст пустой/длина <10 → fit False «слишком короткое»;
|
||||
2) ML: если `ml_client.is_enabled()` и прогноз `take` и `label=='spam'` → fit False «ML: спам»;
|
||||
3) ИИ: если `aiEnabled` → один JSON-вызов `ai_service.chat_json(промпт, user=text)` с промптом из описания задачи и ключей (`{fit, reason}`); ошибка → шаг 4;
|
||||
4) эвристика: fit = любой ключ входит в `clean_short(text)` casefold; reason «совпал ключ "…"» / «нет совпадений с ключами».
|
||||
- `async def evaluate_sample(task: dict, messages: list[dict]) -> dict` — последовательно по каждому сообщению; вернуть `{"fit_count": int, "total": int, "fit_ratio": float, "per_message": [{"text": …, "fit", "reason", "topic_id"}]}`.
|
||||
- `def passed(ev: dict, task: dict) -> bool` — `ev["total"] >= 3 and ev["fit_ratio"]*100 >= task["threshold"]`.
|
||||
|
||||
- [ ] **Step 1: Реализовать модуль**. Промпт ИИ (внутри модуля, константа):
|
||||
`Оцени, относится ли сообщение к сфере/задаче. Описание: {description}. Ключи: {keywords}. Верни JSON {"fit": 0|1, "reason": "краткая причина"}.`
|
||||
|
||||
- [ ] **Step 2: Проверить на временной БД** (без сети): `detect_lang_ru(["Ищем python разработчика"]) is True`; `detect_lang_ru(["we need a python developer"]) is False`; `group_by_topic` объединяет по topic_id и сортирует; `evaluate_message` на задаче без ИИ/ML возвращает эвристический fit по ключу.
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Воркер Discovery (поиск → оценка → авто-вступление)
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/services/discovery_worker.py`
|
||||
- Modify: `backend/app/main.py` (фоновый цикл `_discovery_loop`, каждые 5 c)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `discovery` (Task 3), `tg.discovery_*` (Task 4), `discovery_eval` (Task 5), `ban_guard` (Task 2).
|
||||
- Produces: `async def tick() -> dict` — выполняет ОДНО действие и возвращает `{"action": ..., "taskId": ...}` (или `{"action": "none"}`).
|
||||
|
||||
Логика tick (по одной задаче за вызов, начиная с самой старой running):
|
||||
1. Если задача `search_done=False`: взять ключ `keywords[search_idx]`, вызвать `tg.discovery_search`; для каждого результата `discovery.add_candidate`; `discovery.advance_search(task_id)`; если `search_done` стал True — лог `search` «поиск завершён: N кандидатов». Возврат.
|
||||
2. Иначе взять первого кандидата статуса `new` задачи:
|
||||
- `info = tg.discovery_info`; `participants`, `kind` (forum если `is_forum`); при заданном `min_subscribers` и participants НЕ None и меньше минимума — `set_candidate_status(...)` нет: просто `discovery.delete_candidate` + лог `skip`; если participants None — метка «участники не подтверждены» (идём дальше).
|
||||
- `read = tg.discovery_read(dialog_id, sample_size)`.
|
||||
- Если `read.ok=False` (история недоступна без членства): kind==channel → `review` с меткой «канал: история недоступна»; группа/форум → `review` с меткой «закрытая группа (история скрыта) — вступите сами»; оценка контента не производится, неподтверждённые фильтры помечаются.
|
||||
- Язык: если прочитано и `task.lang=='ru'`: `lang_ru=detect_lang_ru(...)`; False → удалить кандидата, лог `skip` «язык не русский»; None → метка «язык не подтверждён».
|
||||
- Оценка: `evaluate_sample`; `passed` → метки topics/fit → `review` + лог `review`; иначе удалить кандидата, лог `skip` «мало подходящих (X из N)».
|
||||
3. Авто-вступление (отдельный проход tick, приоритет ниже оценки): если у running-задачи `auto_join` и есть кандидат `review` и `ban_guard.can_auto_join()`:
|
||||
- повторная проверка «мы не состоим» (`dialogs`/blacklist) → если вступили уже → `mark_rejected` с логом;
|
||||
- `await ban_guard.wait_join_delay()` (рандом 50–70 с — спейсинг авто-вступлений; ручные join из API паузу не делают);
|
||||
- `tg.discovery_join(username)` → `discovery.mark_joined(dialog_id, auto=True)` → `tg.add_dialog_monitored(...)`; при FloodWaitError → `ban_guard.note_flood()` + лог `flood`.
|
||||
4. Если `task.joined >= task.plan_joins` → статус `done`, лог `done`.
|
||||
|
||||
- [ ] **Step 1: Реализовать** `discovery_worker.py` и цикл в `main.py`.
|
||||
- [ ] **Step 2: Проверить компиляцию** и запуск без падений (воркер с пустыми таблицами делает `none`). Полный прогон — Task 10 вручную.
|
||||
|
||||
---
|
||||
|
||||
### Task 7: API Discovery
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/routers/discovery_routes.py`
|
||||
- Modify: `backend/app/main.py` (регистрация роутера)
|
||||
|
||||
**Interfaces:**
|
||||
- Prefix `/api/discovery`, auth `current_login`:
|
||||
- `GET /tasks`, `POST /tasks`, `PATCH /tasks/{id}`, `DELETE /tasks/{id}`, `POST /tasks/{id}/start`, `POST /tasks/{id}/pause`
|
||||
- `POST /tasks/{id}/generate-keywords` — ИИ: промпт по description → JSON `{"keywords": [...]}` (8–16 строк RU+EN); ИИ недоступен/выключен → `{"keywords": [], "error": "..."}`.
|
||||
- `GET /tasks/{id}/candidates?status=`
|
||||
- `POST /candidates/{dialog_id}/join` — ручное вступление (вне квот): `tg.discovery_join` + `add_dialog_monitored` + `mark_joined(auto=False)`; 400 при ошибке.
|
||||
- `POST /candidates/{dialog_id}/reject` — `mark_rejected` (добавляет в чёрный список). Если кандидат уже `joined` — 400.
|
||||
- `GET /blacklist`, `DELETE /blacklist/{dialog_id}`
|
||||
- `GET /tasks/{id}/log`
|
||||
|
||||
Pydantic-модели: `TaskCreate` (name, description, keywords, minSubscribers, lang, threshold, sampleSize, planJoins, autoJoin), `TaskPatch` (все optional), `GenKeywordsBody` не нужен (id в пути).
|
||||
|
||||
- [ ] **Step 1: Реализовать роутер** (ValueError → HTTPException 400; KeyError → 404).
|
||||
- [ ] **Step 2: Зарегистрировать в main.py**.
|
||||
- [ ] **Step 3: Проверить API на живом контейнере**: логин, создание задачи plan=1, list, delete; `generate-keywords` вернёт error-ветку без настроенного ИИ (не падает).
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Фронтенд — store + каркас подвкладки «Поиск»
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/store.js`
|
||||
- Create: `frontend/src/views/DiscoveryView.vue`
|
||||
- Modify: `frontend/src/views/ChannelsView.vue`
|
||||
|
||||
**Interfaces:**
|
||||
- state: `channelsTab: 'list' | 'search'`, `discTasks: []`, `discCandidates: []`, `discBlacklist: []`, `discLog: []`, `discActiveTaskId: null`, `discCandidateStatus: 'review'`, `discBusy: false`.
|
||||
- store-функции: `gotoChannelsTab(tab)`, `loadDiscTasks()`, `saveDiscTask(form, id=null)` (create/patch), `deleteDiscTask(id)`, `startDiscTask(id)`, `pauseDiscTask(id)`, `generateDiscKeywords(taskId)`, `loadDiscCandidates(taskId, status)`, `joinDiscCandidate(c)`, `rejectDiscCandidate(c)`, `loadDiscBlacklist()`, `removeDiscBlacklist(id)`, `loadDiscLog(taskId)`.
|
||||
|
||||
- [ ] **Step 1: store.js** — состояние + функции (паттерны: `api.get/post/patch/delete`, `toast`, `errMsg`).
|
||||
- [ ] **Step 2: ChannelsView.vue** — в шапке сегмент: «Каналы | Поиск» (`state.channelsTab`), содержимое по табу.
|
||||
- [ ] **Step 3: DiscoveryView.vue (каркас)**: левая колонка — список задач (+ «Новая задача»); правая — панель задачи: мастер (name, description, «Сгенерировать ключи ИИ», чипы ключей редактируемые, minSubscribers, lang select ru/any, threshold, sampleSize, planJoins, autoJoin toggle, кнопки «Запустить/Пауза/Удалить»).
|
||||
- [ ] **Step 4: `npm run build`** — без ошибок.
|
||||
|
||||
---
|
||||
|
||||
### Task 9: Фронтенд — кандидаты, действия, чёрный список, история
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/views/DiscoveryView.vue`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 8 (store).
|
||||
|
||||
- [ ] **Step 1: Табы панели задачи**: «В обработке» (`new`) / «На рассмотрении» (`review`) / «Вступили» (`joined`) / «Отклонены» (`rejected`) + «История» (лог). Бейджи счётчиков скрыты при 0.
|
||||
- [ ] **Step 2: Карточка кандидата**: название, @username, kind-иконка/метка (канал/группа/форум), метки marks (чипы: закрытая, не прочитан, участники/язык не подтверждены, мало сообщений), участники, «подходит X из N», кнопки «Вступить и мониторить» / «Отклонить» (только для review). Форум → раскрывающийся список topics («тема — подходит X из N»).
|
||||
- [ ] **Step 3: Чёрный список** (под списками или отдельный таб) — снять источник; «Настройки квот» — popover/inline с `discJoinLimit/discJoinDelayMin/discJoinDelayMax` + стоп-кран (PATCH /api/settings).
|
||||
- [ ] **Step 4: История** — лог задачи.
|
||||
- [ ] **Step 5: `npm run build`** — без ошибок; визуальная проверка основных сценариев (Task 10).
|
||||
|
||||
---
|
||||
|
||||
### Task 10: ТЗ, сборка и end-to-end проверка
|
||||
|
||||
**Files:**
|
||||
- Modify: `ТЗ.md` (раздел «Поиск и подключение каналов»)
|
||||
|
||||
- [ ] **Step 1: Дополнить ТЗ** — кратко: задачи поиска, каскад фильтров, глобальное правило «мы не состоим», метки, оценка по темам, review/join/reject + чёрный список, авто-вступление и квоты (50/сутки, 50–70 с), подвкладка «Поиск» на «Каналах».
|
||||
- [ ] **Step 2: Сборка и рестарт**:
|
||||
|
||||
```bash
|
||||
docker compose build app && docker compose up -d app
|
||||
cd frontend && npm run build
|
||||
```
|
||||
|
||||
- [ ] **Step 3: E2E вручную (нужен подключённый Telegram-аккаунт)**:
|
||||
1. «Каналы → Поиск» → создать задачу (описание, план 3, авто-вступление выкл) → «Сгенерировать ключи» → запустить.
|
||||
2. Дождаться кандидатов; проверить, что текущие подписки и отклонённые не появляются.
|
||||
3. Открыть кандидата: метки, участники, fit «X из N», темы форума.
|
||||
4. «Вступить и мониторить» → источник появляется в «Каналах» (monitor on) и начинает давать карточки.
|
||||
5. «Отклонить» → уходит в чёрный список; повторно не находится.
|
||||
6. Включить авто-вступление: проверить паузы (≥50 с) и расход суточного лимита.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- **Покрытие спеки:** Task 1 (хранилище+настройки), Task 2 (квоты/анти-бан), Task 3 (задачи/бюджет планов/чёрный список), Task 4 (поиск/инфо/чтение/join), Task 5 (язык/темы/fit), Task 6 (воркер+авто-join), Task 7 (API), Task 8–9 (UI), Task 10 (ТЗ+E2E). Правило «мы не состоим» — Task 3 `add_candidate`, Task 6 шаг 3 (повторная проверка перед join), UI Task 9. Разделы спеки §4–§12 покрыты; «вне рамок» (§13) не реализуются.
|
||||
- **Плейсхолдеры:** нет; у каждого шага есть конкретный код/поведение и способ проверки.
|
||||
- **Согласованность:** единые статусы задач `draft|running|paused|done|failed`, кандидатов `new|review|joined|rejected`; события лога `search|found|skip|eval|review|join_auto|join_manual|leave|reject|flood|error|done`; все имена настроек и функций совпадают между задачами.
|
||||
|
||||
@@ -1,173 +1,173 @@
|
||||
# Дейл (Deal) — Roadmap этапов (все этапы 0–7 выполнены; 2026-09-08)
|
||||
|
||||
> Исторический документ (roadmap этапов 0–7, 2026-09-08). Актуальное состояние — `docs/superpowers/STATUS.md` и `docs/technical/Техническая-документация-Дейл.md`.
|
||||
|
||||
> Назначение: зафиксировать план продолжения разработки «Дейл» по этапам. Каждый этап исполняется
|
||||
> как отдельный SDD-план (файл в `docs/superpowers/plans/`, ledger в `.superpowers/sdd/<plan>/`),
|
||||
> задача-за-задачей с ревью. Проект НЕ git — фиксация в отчётах и ledgers.
|
||||
|
||||
## Выполнено
|
||||
|
||||
- **Этап 0 — Каркас** (`2026-09-05-deal-scaffold.md`): структура `src/`, фронт переехал в `src/frontend/`,
|
||||
`.editorconfig`+`Directory.Build.props`, `Deal.sln` (11 проектов), тесты, dev-Postgres (:5433),
|
||||
tenant-контекст (`TenantId`/`ITenantContext`/`TenantContext` AsyncLocal/`ConnectionStringProvider` search_path),
|
||||
EF (public), `TenantSchemaMigrator`, CI-скрипты.
|
||||
- **Этап 1 — Доступ и мультитенантность** (`2026-09-05-deal-stage1-tenancy.md`): карта `/api`
|
||||
(`docs/api/api-map.md`, 101 эндпоинт, 87 использует фронт), два DbContext (системный `public` + бессхемный
|
||||
tenant: таблица `settings`), миграции InitialSystem/InitialTenant, модуль Tenants (порты+адаптеры,
|
||||
Argon2id, сессии 30 дней), auth-эндпоинты 1:1 (login/logout/me/change-password), SessionMiddleware +
|
||||
`ITenantContext.Reset`, `TenantProvisioningService` (схема `tenant_<32hex>` + Migrate c
|
||||
`MigrationsHistoryTable("__TenantMigrationsHistory", schema)`), `TenantBootstrapService` (seed: тенант
|
||||
id `000…001` + admin/admin из env; идемпотентно). 25 тестов PASS.
|
||||
- **Этап 2 — Settings (настройки тенанта)** (`2026-09-05-deal-stage2-settings.md`): модуль
|
||||
`Deal.Modules.Settings` (каталог ключей/дефолты 1:1 с прототипом, ISettingsStore, SettingsService
|
||||
снимок+PATCH 1:1, IncomingRules, PromptFiller, RatesService) + адаптеры (SettingsStore на `settings`,
|
||||
AesGcmSecretCipher), эндпоинты GET/PATCH `/api/settings`, POST `/api/ai/check`, GET `/api/rates`,
|
||||
POST `/api/rates/refresh`, `/api/ml/*` (заглушка LocalMlClient), POST `/api/admin/check-message`
|
||||
(тестер фильтров). Секреты AI/Telegram — AES-GCM (`enc:` в БД, ключ env `DEAL_ENCRYPTION_KEY`/файл).
|
||||
Задачи 1–11 приняты: 175 unit-тестов PASS, build 0/0, сквозная curl-приёмка :5080 PASS=60 FAIL=0 +
|
||||
psql (шифрование, внутренние ключи не публикуются). **Ограничение:** Settings-экран обслуживается
|
||||
бэкендом, но Vue-фронт полностью оживает только с этапом 3 (его `boot()` требует `/api/boards`,
|
||||
`/api/leads`, `/api/projects`, `/api/tg/status`, `/api/columns/state`; Telegram-вкладка, кнопки
|
||||
«Проверить правила сейчас»/«Пересобрать индекс», «Предложить ключи» и канбан-фронт — этапы 3–6).
|
||||
|
||||
- **Этап 3 — Kanban (дашборд): колонки, карточки, архив/корзина** (`2026-09-05-deal-stage3-kanban.md`):
|
||||
миграция TenantKanban (Boards/Cards/LeadComments/CardMoves/MlOutbox в схеме тенанта), модуль
|
||||
`Deal.Modules.Kanban` (доски/карточки/правила `ColumnRules` с matchHits, StorageTickService +
|
||||
фоновый StorageTickScheduler 30 с, ConversionRecomputer, демо-фабрика, эвристика ИИ-предложений),
|
||||
эндпоинты boards/columns/leads/search/events(SSE)/admin/demo/ai-suggest, boot-заглушки /projects и
|
||||
/tg/status, LocalMlClient+PushAsync. Задачи 1–15 приняты: 410 unit-тестов PASS, build 0/0, сквозная
|
||||
curl-приёмка :5080 PASS=94 FAIL=0 + psql. **Ограничения:** pipeline/очередь/отсев/FTS — этап 4;
|
||||
projects/файлы/reminder_due — этап 5; реальные ai/telegram/ml и discovery — этап 6; reclassify и
|
||||
/admin/fts/rebuild — контракт-заглушки. Фронт теперь boot'ится полностью и канбан-дашборд работает
|
||||
на демо-данных (реальные данные появятся с pipeline этапа 4).
|
||||
- **Этап 4 — Pipeline + вкладка «Обработка»** (`2026-09-05-deal-stage4-pipeline.md`): миграция
|
||||
TenantPipeline (QueueItems/RejectedItems/DedupEntries + FTS-колонки SearchTsv на Cards/RejectedItems),
|
||||
модуль `Deal.Modules.Pipeline` (ядро разбора 1:1, Ingest/ProcessingService/PipelineWorkerService
|
||||
pump 1:1, CardComposer через `IKanjStore.AddCardAsync`, dedup-связь), порт `IAiClassifier` +
|
||||
детерминированный `LocalAiClassifier`, эндпоинты `/api/pipeline/*` + демо-ingest, реальные
|
||||
admin/tick и admin/fts/rebuild (+SSE new_lead/тост очистки отсева), фоновые PipelineWorkerScheduler
|
||||
(2 с) и purge-отсева 3 дня в StorageTickScheduler, FTS-поиск `/api/search`. Задачи 1–13 приняты:
|
||||
**535 unit-тестов PASS**, build 0/0, сквозная curl-приёмка :5080 (Task 13 — финал) PASS=74 FAIL=0
|
||||
+ psql. **Ограничения:** projects/reminder_due/файлы — этап 5; реальные ai/telegram/ml-сервисы и
|
||||
их gRPC-ингресс, discovery, ИИ-предложения колонок на реальных данных — этап 6; оператор/лимиты/
|
||||
аудит — этап 7. Вкладка «Обработка» и канбан-дашборд работают на реальном конвейере (демо-ingest
|
||||
до telegram-этапа 6).
|
||||
- **Этап 5 — Projects («Выбранные»): стадии, напоминания, файлы/ссылки, история**
|
||||
(`2026-09-05-deal-stage5-projects.md`): миграция TenantProjects (`ProjectCards` с partial UNIQUE
|
||||
LeadId), модуль `Deal.Modules.Projects` (стадии ProjectStages 1:1, ProjectsService: take из дашборда с
|
||||
уходом лида в `col='taken'`/ручное создание/patch presence-aware/move+история/clear-rejected/
|
||||
комментарии/ссылки; ProjectReminderService; ProjectFilesService), порт `IFileStorage` + адаптеры
|
||||
`LocalFileStorage`/`MinioFileStorage` (deal-minio в compose.dev.yml), эндпоинты `/api/projects*`
|
||||
(16 шт., файлы и напоминания включены), boot-заглушка /projects снята, напоминания в admin/tick +
|
||||
фоновый 30-с цикл StorageTickScheduler + SSE `reminder_due`. Задачи 1–13 приняты: **620 unit-тестов
|
||||
PASS**, build 0/0, сквозная curl-приёмка :5080 (Task 13 — финал) PASS=75 FAIL=0 + psql (take-
|
||||
семантика, история, clear-rejected, UNIQUE LeadId, файлы на диске, reminder_due фоновым циклом).
|
||||
**Ограничения:** реальные ai/telegram/ml-сервисы и их gRPC-ингресс, discovery, telegram-вкладка и
|
||||
`/tg/status` — этап 6; оператор/лимиты/админка, мульти-аренда MinIO-бакетов — этап 7.
|
||||
- **Этап 6 — Сервисы telegram/ai/ml (отдельные процессы) + Discovery** (`2026-09-05-deal-stage6-services.md`):
|
||||
gRPC-контракты в `src/contracts/*.proto` (общий `Deal.Proto`); три автономных процесса — telegram-service
|
||||
(:5101: ферма сессий 1 акк/тенант, QR-вход, AES-GCM-сессии `/data/sessions`, диалоги/мониторинг/backfill
|
||||
с анти-бан-паузами), ai-service (:5102: LLM-фасад OpenAI-совместимых+Anthropic, Filter/Classify/
|
||||
GenerateKeywords/EvaluateFit, usage), ml-service (:5103: инкрементальный наивный Байес 1:1 с python
|
||||
`mlservice/model.py`, SQLite на тенанта); core — gRPC-ингресс telegram :5082 (PushMessage→очередь,
|
||||
SyncDialogs, ReportStatus→SSE), модуль Telegram (Dialogs/TgMessages) + эндпоинты /api/tg (14 шт., реальный
|
||||
статус, QR-SVG) вместо boot-заглушки, ai/ml-gRPC-адаптеры за флагами `Services:*:UseLocal` (код-дефолт
|
||||
Local, compose.dev.yml — false), MlOutboxFlushScheduler (10 с), модуль Discovery (воркер 5 с: поиск/
|
||||
каскад оценки/авто-join с квотами и бан-гардом; /api/discovery 13 шт.); полный dev-стек —
|
||||
`deploy/compose.dev.yml` (postgres/minio/3 сервиса/core, secrets, healthcheck), smoke-скрипт
|
||||
`scripts/dev-smoke.sh` (отложенный живой прогон — Docker Desktop был выключен). Задачи 1–20 приняты:
|
||||
**830 unit-тестов PASS**, 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-приёмки. Ledger:
|
||||
`.superpowers/sdd/deal-stage6-services/`. **Ручные проверки (с кредами):** Telegram-вход
|
||||
(api_id/api_hash/QR) и реальные LLM-вызовы; живой smoke `scripts/dev-smoke.sh` — после поднятия Docker.
|
||||
**Ограничения этапа 6 (переходят в этап 7):** mTLS-сертификаты и prod-compose; лимиты/бюджеты токенов
|
||||
(учёт `aiTokenUsage` уже есть); оператор/админка/аудит-поток; rate limiting gRPC; экспорт/импорт
|
||||
ML-моделей; ротация/бэкап ключей сессий; reclassify на реальном ИИ (контракт-заглушка остаётся).
|
||||
|
||||
- **Этап 7 — SaaS-контур (Tasks 1–16, 2026-09-08)** (`2026-09-05-deal-stage7-saas.md`): оператор
|
||||
(`public`-таблицы, кука `deal_operator_session`, bootstrap env, ручки `/api/operator/*` — API-only),
|
||||
инвайты + активация `POST /api/join`, лимиты ИИ-бюджета с fallback-декораторами и SSE-тостами,
|
||||
append-only аудит-поток, rate limiting (приложение + gRPC-ингресс + защита входа), Origin-проверка/
|
||||
security-заголовки/ForwardedHeaders, mTLS за флагом DEAL_MTLS_* + `scripts/mtls-certs.sh`, Serilog JSON
|
||||
во всех 4 процессах, prod-деплой `deploy/compose.prod.yml` (caddy 80/443, mTLS-env, healthcheck'и
|
||||
grpc_health_probe, профиль observability: promtail/loki/grafana) + `deploy/.env.prod.example`,
|
||||
бэкапы `scripts/backup.sh`/`restore.sh` (+ `deal-backup-lib.sh`). **Task 16 (финал)**: актуализированы
|
||||
техдок §5/§7–§11/§13 (фактический стек, Manual-пометки), api-map (раздел «Реализовано в Deal»),
|
||||
user-guide, STATUS.md (этапы 0–7 = 100%). Финальный прогон: core **1123/1123 PASS**, telegram 114/114,
|
||||
ai 50/50, ml 36/36 PASS, build 0/0 всех четырёх sln, `compose.prod.yml config` rc=0, `sh -n`
|
||||
скриптов rc=0. Ledger: `.superpowers/sdd/deal-stage7-saas/`.
|
||||
**Manual (нужен docker/живые креды):** применение system-миграции + сквозная SaaS-curl-приёмка,
|
||||
подъём compose.prod и dev-smoke `scripts/dev-smoke.sh`, mTLS-рукопожатие контейнеров, реальные
|
||||
Telegram/LLM-вызовы, прогон `scripts/backup.sh` и restore-тест — чек-лист в task-16-report.md.
|
||||
|
||||
## Эталонные конвенции (уже в коде — их придерживаться дальше)
|
||||
|
||||
- Модуль = чистый проект (SharedKernel/Contracts): порты (интерфейсы) + record-DTO, без EF.
|
||||
Регистрация: `AddTenantsModule()` (модуль), адаптеры EF — в `Deal.Infrastructure` через
|
||||
`AddDealPersistence()` (scoped). HTTP-эндпоинты — в `Deal.Api/Endpoints/*` (`MapXxxEndpoints`).
|
||||
- Два EF-контекста: системный (public, явная схема) и tenant (бессхемный; новые таблицы модулей —
|
||||
DbSet в `TenantDbContext` + `dotnet ef migrations add X --context TenantDbContext`; применяются
|
||||
провижинером ко всем схемам). Ошибки API — `{detail}`; JSON camelCase; кука `deal_session`.
|
||||
- Константы/настройки: `IOptions<T>`; без магических чисел; 1 тип=1 файл; XML-doc на public.
|
||||
|
||||
## Следующие этапы (после этапов 0–7; порядок из архитектуры §12.5)
|
||||
|
||||
> **Актуальный источник отложенного и техдолга — `backlog.md` в корне** (роудмап черпается оттуда).
|
||||
> Ниже — историческая секция роудмапа.
|
||||
|
||||
> Этапы 0–7 выполнены (см. «Выполнено»). Ниже — следующие инкременты: заделы этапа 7 (сознательно
|
||||
> вынесены, подробно — техдок §11) и пункты архитектуры, не входившие в этапы.
|
||||
|
||||
### Этап 8+ — следующие инкременты (заделы этапа 7, подробно — техдок §11):
|
||||
- **Этапы 8–10 выполнены** (2026-09-10): ревью/качество; единая карточка (unified card);
|
||||
оператор-консоль + активация инвайта (UI) + аудит действий и аналитика расхода токенов + ELK/Loki-дашборды.
|
||||
- Остаются заделы: OTel-метрики/Prometheus и дашборды метрик (сейчас Serilog-логи → Loki);
|
||||
multi-instance rate-limit и бэкенд попыток входа; экспорт/импорт ML-моделей; reclassify на реальном ИИ;
|
||||
мультиаккаунтность Telegram; биллинг/планы; k8s/Cloudflare-конфигурация; purge-автоматика audit_log.
|
||||
|
||||
### Этап 11 — Локализация интерфейса (i18n)
|
||||
|
||||
**Требование владельца (2026-09-10).** Весь интерфейс — на русском; все тексты вынесены в ресурсы,
|
||||
чтобы можно было добавлять новые языки и менять язык **на лету**.
|
||||
|
||||
- **Русский — язык по умолчанию.** Все пользовательские строки UI (экраны, кнопки, подписи, пустые
|
||||
состояния, подсказки, подтверждения, уведомления/тосты, страницы оператора и активации) — на русском.
|
||||
- **Никакого хардкода строк в компонентах.** Все тексты — в словарях ресурсов (ключ → значение),
|
||||
включая сообщения об ошибках, которые сейчас формируются на бэке (`{detail}`), — они должны быть
|
||||
локализуемы (ключ + параметры) или переводимы по коду.
|
||||
- **Переключение языка на лету**, без перезагрузки страницы; выбранный язык сохраняется (localStorage/настройки).
|
||||
- **Расширяемость:** добавление нового языка = новый файл словаря, без правок компонентов.
|
||||
- **Форматирование** дат/времени/чисел/валют — через i18n-форматтеры (не вручную), плюрализация —
|
||||
через правила языка.
|
||||
- Ключи — стабильные, сгруппированные по областям (nav/cards/settings/operator/…); отсутствующий
|
||||
ключ в языке → фолбэк на русский.
|
||||
- Бэк: ответы API остаются с `{detail}`/кодами; фронт отображает локализованный текст по коду/ключу
|
||||
(при необходимости — расширяемый словарь ошибок).
|
||||
|
||||
UI-область, к которой это применяется: основное приложение (дашборд, «Выбранные», настройки, каналы,
|
||||
обработка) и оператор-консоль (этап 10).
|
||||
|
||||
## Открытые точки согласования (накопились к концу этапа 1)
|
||||
|
||||
> Решения владельца (2026-09-06): 1 — бренд меняем (сделано точечно: index.html, LoginView, Sidebar, DiscoveryView, SettingsView); 2 — инвайты/оператор остаются на SaaS-этап, dev-seed admin/admin; 3 — PascalCase — конвенция БД; 4 — кука `deal_session` остаётся; 5 — заглушки сервисов допустимы (порты с детерминированными локальными реализациями до этапов 6+); 6 — идём по roadmap все этапы.
|
||||
|
||||
1. **Бренд во фронте**: Vue-фронт всё ещё показывает «LeadRadar» (LoginView, заголовки). Фронт
|
||||
«не трогаем» — но бренд теперь «Дейл». Менять ли строки бренда во фронте (точечно) или позже?
|
||||
2. **Инвайты/оператор**: ТЗ требует invite-only + отдельный вход оператора; во фронте такого UI нет.
|
||||
Оставляем dev-seed (admin/admin + дефолтный тенант) до этапа 7? Тогда auth остаётся «как прототип».
|
||||
3. **Имена колонок БД**: EF генерирует PascalCase (`UpdatedAt`), ТЗ/доки местами в SQL-нотации
|
||||
(snake_case). Оставляем PascalCase (конвенция кода) — подтвердить.
|
||||
4. **Имя куки** `deal_session` (прототип: `leadradar_session`) — фронту всё равно; ок?
|
||||
5. **Сервисы ml/ai/telegram**: полный ребилд — это самые крупные этапы. Подтвердить, что локально
|
||||
между этапами допустимы порты с детерминированными заглушками (пока сервисы не написаны), чтобы
|
||||
pipeline/kanban разрабатывать и показывать на синтетических входах.
|
||||
6. **Приоритет и темп**: продолжать строго по roadmap (Settings → Kanban → Pipeline → Projects →
|
||||
сервисы+Discovery → SaaS-контур) и в каком темпе/объёме на сессию.
|
||||
|
||||
**Статус на 2026-09-08 (этапы 0–7 завершены):** п.2 **закрыт** — инвайты/оператор реализованы в этапе 7
|
||||
(API-only: `/api/operator/*` + `/api/join`), dev-seed `admin/admin` остаётся dev-only (Ruling 1);
|
||||
UI операторской админки и страницы активации — задел этапа 8+. п.1 — бренд во фронте меняется точечно
|
||||
(как решено); п.3–5 — зафиксированы решениями (PascalCase, `deal_session`, локальные реализации портов)
|
||||
и не пересматривались; п.6 — roadmap исполнен: этапы 0–7 пройдены (STATUS.md — 100%, Manual-чек-лист
|
||||
вынесен отдельно).
|
||||
# Дейл (Deal) — Roadmap этапов (все этапы 0–7 выполнены; 2026-09-08)
|
||||
|
||||
> Исторический документ (roadmap этапов 0–7, 2026-09-08). Актуальное состояние — `docs/superpowers/STATUS.md` и `docs/technical/Техническая-документация-Дейл.md`.
|
||||
|
||||
> Назначение: зафиксировать план продолжения разработки «Дейл» по этапам. Каждый этап исполняется
|
||||
> как отдельный SDD-план (файл в `docs/superpowers/plans/`, ledger в `.superpowers/sdd/<plan>/`),
|
||||
> задача-за-задачей с ревью. Проект НЕ git — фиксация в отчётах и ledgers.
|
||||
|
||||
## Выполнено
|
||||
|
||||
- **Этап 0 — Каркас** (`2026-09-05-deal-scaffold.md`): структура `src/`, фронт переехал в `src/frontend/`,
|
||||
`.editorconfig`+`Directory.Build.props`, `Deal.sln` (11 проектов), тесты, dev-Postgres (:5433),
|
||||
tenant-контекст (`TenantId`/`ITenantContext`/`TenantContext` AsyncLocal/`ConnectionStringProvider` search_path),
|
||||
EF (public), `TenantSchemaMigrator`, CI-скрипты.
|
||||
- **Этап 1 — Доступ и мультитенантность** (`2026-09-05-deal-stage1-tenancy.md`): карта `/api`
|
||||
(`docs/api/api-map.md`, 101 эндпоинт, 87 использует фронт), два DbContext (системный `public` + бессхемный
|
||||
tenant: таблица `settings`), миграции InitialSystem/InitialTenant, модуль Tenants (порты+адаптеры,
|
||||
Argon2id, сессии 30 дней), auth-эндпоинты 1:1 (login/logout/me/change-password), SessionMiddleware +
|
||||
`ITenantContext.Reset`, `TenantProvisioningService` (схема `tenant_<32hex>` + Migrate c
|
||||
`MigrationsHistoryTable("__TenantMigrationsHistory", schema)`), `TenantBootstrapService` (seed: тенант
|
||||
id `000…001` + admin/admin из env; идемпотентно). 25 тестов PASS.
|
||||
- **Этап 2 — Settings (настройки тенанта)** (`2026-09-05-deal-stage2-settings.md`): модуль
|
||||
`Deal.Modules.Settings` (каталог ключей/дефолты 1:1 с прототипом, ISettingsStore, SettingsService
|
||||
снимок+PATCH 1:1, IncomingRules, PromptFiller, RatesService) + адаптеры (SettingsStore на `settings`,
|
||||
AesGcmSecretCipher), эндпоинты GET/PATCH `/api/settings`, POST `/api/ai/check`, GET `/api/rates`,
|
||||
POST `/api/rates/refresh`, `/api/ml/*` (заглушка LocalMlClient), POST `/api/admin/check-message`
|
||||
(тестер фильтров). Секреты AI/Telegram — AES-GCM (`enc:` в БД, ключ env `DEAL_ENCRYPTION_KEY`/файл).
|
||||
Задачи 1–11 приняты: 175 unit-тестов PASS, build 0/0, сквозная curl-приёмка :5080 PASS=60 FAIL=0 +
|
||||
psql (шифрование, внутренние ключи не публикуются). **Ограничение:** Settings-экран обслуживается
|
||||
бэкендом, но Vue-фронт полностью оживает только с этапом 3 (его `boot()` требует `/api/boards`,
|
||||
`/api/leads`, `/api/projects`, `/api/tg/status`, `/api/columns/state`; Telegram-вкладка, кнопки
|
||||
«Проверить правила сейчас»/«Пересобрать индекс», «Предложить ключи» и канбан-фронт — этапы 3–6).
|
||||
|
||||
- **Этап 3 — Kanban (дашборд): колонки, карточки, архив/корзина** (`2026-09-05-deal-stage3-kanban.md`):
|
||||
миграция TenantKanban (Boards/Cards/LeadComments/CardMoves/MlOutbox в схеме тенанта), модуль
|
||||
`Deal.Modules.Kanban` (доски/карточки/правила `ColumnRules` с matchHits, StorageTickService +
|
||||
фоновый StorageTickScheduler 30 с, ConversionRecomputer, демо-фабрика, эвристика ИИ-предложений),
|
||||
эндпоинты boards/columns/leads/search/events(SSE)/admin/demo/ai-suggest, boot-заглушки /projects и
|
||||
/tg/status, LocalMlClient+PushAsync. Задачи 1–15 приняты: 410 unit-тестов PASS, build 0/0, сквозная
|
||||
curl-приёмка :5080 PASS=94 FAIL=0 + psql. **Ограничения:** pipeline/очередь/отсев/FTS — этап 4;
|
||||
projects/файлы/reminder_due — этап 5; реальные ai/telegram/ml и discovery — этап 6; reclassify и
|
||||
/admin/fts/rebuild — контракт-заглушки. Фронт теперь boot'ится полностью и канбан-дашборд работает
|
||||
на демо-данных (реальные данные появятся с pipeline этапа 4).
|
||||
- **Этап 4 — Pipeline + вкладка «Обработка»** (`2026-09-05-deal-stage4-pipeline.md`): миграция
|
||||
TenantPipeline (QueueItems/RejectedItems/DedupEntries + FTS-колонки SearchTsv на Cards/RejectedItems),
|
||||
модуль `Deal.Modules.Pipeline` (ядро разбора 1:1, Ingest/ProcessingService/PipelineWorkerService
|
||||
pump 1:1, CardComposer через `IKanjStore.AddCardAsync`, dedup-связь), порт `IAiClassifier` +
|
||||
детерминированный `LocalAiClassifier`, эндпоинты `/api/pipeline/*` + демо-ingest, реальные
|
||||
admin/tick и admin/fts/rebuild (+SSE new_lead/тост очистки отсева), фоновые PipelineWorkerScheduler
|
||||
(2 с) и purge-отсева 3 дня в StorageTickScheduler, FTS-поиск `/api/search`. Задачи 1–13 приняты:
|
||||
**535 unit-тестов PASS**, build 0/0, сквозная curl-приёмка :5080 (Task 13 — финал) PASS=74 FAIL=0
|
||||
+ psql. **Ограничения:** projects/reminder_due/файлы — этап 5; реальные ai/telegram/ml-сервисы и
|
||||
их gRPC-ингресс, discovery, ИИ-предложения колонок на реальных данных — этап 6; оператор/лимиты/
|
||||
аудит — этап 7. Вкладка «Обработка» и канбан-дашборд работают на реальном конвейере (демо-ingest
|
||||
до telegram-этапа 6).
|
||||
- **Этап 5 — Projects («Выбранные»): стадии, напоминания, файлы/ссылки, история**
|
||||
(`2026-09-05-deal-stage5-projects.md`): миграция TenantProjects (`ProjectCards` с partial UNIQUE
|
||||
LeadId), модуль `Deal.Modules.Projects` (стадии ProjectStages 1:1, ProjectsService: take из дашборда с
|
||||
уходом лида в `col='taken'`/ручное создание/patch presence-aware/move+история/clear-rejected/
|
||||
комментарии/ссылки; ProjectReminderService; ProjectFilesService), порт `IFileStorage` + адаптеры
|
||||
`LocalFileStorage`/`MinioFileStorage` (deal-minio в compose.dev.yml), эндпоинты `/api/projects*`
|
||||
(16 шт., файлы и напоминания включены), boot-заглушка /projects снята, напоминания в admin/tick +
|
||||
фоновый 30-с цикл StorageTickScheduler + SSE `reminder_due`. Задачи 1–13 приняты: **620 unit-тестов
|
||||
PASS**, build 0/0, сквозная curl-приёмка :5080 (Task 13 — финал) PASS=75 FAIL=0 + psql (take-
|
||||
семантика, история, clear-rejected, UNIQUE LeadId, файлы на диске, reminder_due фоновым циклом).
|
||||
**Ограничения:** реальные ai/telegram/ml-сервисы и их gRPC-ингресс, discovery, telegram-вкладка и
|
||||
`/tg/status` — этап 6; оператор/лимиты/админка, мульти-аренда MinIO-бакетов — этап 7.
|
||||
- **Этап 6 — Сервисы telegram/ai/ml (отдельные процессы) + Discovery** (`2026-09-05-deal-stage6-services.md`):
|
||||
gRPC-контракты в `src/contracts/*.proto` (общий `Deal.Proto`); три автономных процесса — telegram-service
|
||||
(:5101: ферма сессий 1 акк/тенант, QR-вход, AES-GCM-сессии `/data/sessions`, диалоги/мониторинг/backfill
|
||||
с анти-бан-паузами), ai-service (:5102: LLM-фасад OpenAI-совместимых+Anthropic, Filter/Classify/
|
||||
GenerateKeywords/EvaluateFit, usage), ml-service (:5103: инкрементальный наивный Байес 1:1 с python
|
||||
`mlservice/model.py`, SQLite на тенанта); core — gRPC-ингресс telegram :5082 (PushMessage→очередь,
|
||||
SyncDialogs, ReportStatus→SSE), модуль Telegram (Dialogs/TgMessages) + эндпоинты /api/tg (14 шт., реальный
|
||||
статус, QR-SVG) вместо boot-заглушки, ai/ml-gRPC-адаптеры за флагами `Services:*:UseLocal` (код-дефолт
|
||||
Local, compose.dev.yml — false), MlOutboxFlushScheduler (10 с), модуль Discovery (воркер 5 с: поиск/
|
||||
каскад оценки/авто-join с квотами и бан-гардом; /api/discovery 13 шт.); полный dev-стек —
|
||||
`deploy/compose.dev.yml` (postgres/minio/3 сервиса/core, secrets, healthcheck), smoke-скрипт
|
||||
`scripts/dev-smoke.sh` (отложенный живой прогон — Docker Desktop был выключен). Задачи 1–20 приняты:
|
||||
**830 unit-тестов PASS**, 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-приёмки. Ledger:
|
||||
`.superpowers/sdd/deal-stage6-services/`. **Ручные проверки (с кредами):** Telegram-вход
|
||||
(api_id/api_hash/QR) и реальные LLM-вызовы; живой smoke `scripts/dev-smoke.sh` — после поднятия Docker.
|
||||
**Ограничения этапа 6 (переходят в этап 7):** mTLS-сертификаты и prod-compose; лимиты/бюджеты токенов
|
||||
(учёт `aiTokenUsage` уже есть); оператор/админка/аудит-поток; rate limiting gRPC; экспорт/импорт
|
||||
ML-моделей; ротация/бэкап ключей сессий; reclassify на реальном ИИ (контракт-заглушка остаётся).
|
||||
|
||||
- **Этап 7 — SaaS-контур (Tasks 1–16, 2026-09-08)** (`2026-09-05-deal-stage7-saas.md`): оператор
|
||||
(`public`-таблицы, кука `deal_operator_session`, bootstrap env, ручки `/api/operator/*` — API-only),
|
||||
инвайты + активация `POST /api/join`, лимиты ИИ-бюджета с fallback-декораторами и SSE-тостами,
|
||||
append-only аудит-поток, rate limiting (приложение + gRPC-ингресс + защита входа), Origin-проверка/
|
||||
security-заголовки/ForwardedHeaders, mTLS за флагом DEAL_MTLS_* + `scripts/mtls-certs.sh`, Serilog JSON
|
||||
во всех 4 процессах, prod-деплой `deploy/compose.prod.yml` (caddy 80/443, mTLS-env, healthcheck'и
|
||||
grpc_health_probe, профиль observability: promtail/loki/grafana) + `deploy/.env.prod.example`,
|
||||
бэкапы `scripts/backup.sh`/`restore.sh` (+ `deal-backup-lib.sh`). **Task 16 (финал)**: актуализированы
|
||||
техдок §5/§7–§11/§13 (фактический стек, Manual-пометки), api-map (раздел «Реализовано в Deal»),
|
||||
user-guide, STATUS.md (этапы 0–7 = 100%). Финальный прогон: core **1123/1123 PASS**, telegram 114/114,
|
||||
ai 50/50, ml 36/36 PASS, build 0/0 всех четырёх sln, `compose.prod.yml config` rc=0, `sh -n`
|
||||
скриптов rc=0. Ledger: `.superpowers/sdd/deal-stage7-saas/`.
|
||||
**Manual (нужен docker/живые креды):** применение system-миграции + сквозная SaaS-curl-приёмка,
|
||||
подъём compose.prod и dev-smoke `scripts/dev-smoke.sh`, mTLS-рукопожатие контейнеров, реальные
|
||||
Telegram/LLM-вызовы, прогон `scripts/backup.sh` и restore-тест — чек-лист в task-16-report.md.
|
||||
|
||||
## Эталонные конвенции (уже в коде — их придерживаться дальше)
|
||||
|
||||
- Модуль = чистый проект (SharedKernel/Contracts): порты (интерфейсы) + record-DTO, без EF.
|
||||
Регистрация: `AddTenantsModule()` (модуль), адаптеры EF — в `Deal.Infrastructure` через
|
||||
`AddDealPersistence()` (scoped). HTTP-эндпоинты — в `Deal.Api/Endpoints/*` (`MapXxxEndpoints`).
|
||||
- Два EF-контекста: системный (public, явная схема) и tenant (бессхемный; новые таблицы модулей —
|
||||
DbSet в `TenantDbContext` + `dotnet ef migrations add X --context TenantDbContext`; применяются
|
||||
провижинером ко всем схемам). Ошибки API — `{detail}`; JSON camelCase; кука `deal_session`.
|
||||
- Константы/настройки: `IOptions<T>`; без магических чисел; 1 тип=1 файл; XML-doc на public.
|
||||
|
||||
## Следующие этапы (после этапов 0–7; порядок из архитектуры §12.5)
|
||||
|
||||
> **Актуальный источник отложенного и техдолга — `backlog.md` в корне** (роудмап черпается оттуда).
|
||||
> Ниже — историческая секция роудмапа.
|
||||
|
||||
> Этапы 0–7 выполнены (см. «Выполнено»). Ниже — следующие инкременты: заделы этапа 7 (сознательно
|
||||
> вынесены, подробно — техдок §11) и пункты архитектуры, не входившие в этапы.
|
||||
|
||||
### Этап 8+ — следующие инкременты (заделы этапа 7, подробно — техдок §11):
|
||||
- **Этапы 8–10 выполнены** (2026-09-10): ревью/качество; единая карточка (unified card);
|
||||
оператор-консоль + активация инвайта (UI) + аудит действий и аналитика расхода токенов + ELK/Loki-дашборды.
|
||||
- Остаются заделы: OTel-метрики/Prometheus и дашборды метрик (сейчас Serilog-логи → Loki);
|
||||
multi-instance rate-limit и бэкенд попыток входа; экспорт/импорт ML-моделей; reclassify на реальном ИИ;
|
||||
мультиаккаунтность Telegram; биллинг/планы; k8s/Cloudflare-конфигурация; purge-автоматика audit_log.
|
||||
|
||||
### Этап 11 — Локализация интерфейса (i18n)
|
||||
|
||||
**Требование владельца (2026-09-10).** Весь интерфейс — на русском; все тексты вынесены в ресурсы,
|
||||
чтобы можно было добавлять новые языки и менять язык **на лету**.
|
||||
|
||||
- **Русский — язык по умолчанию.** Все пользовательские строки UI (экраны, кнопки, подписи, пустые
|
||||
состояния, подсказки, подтверждения, уведомления/тосты, страницы оператора и активации) — на русском.
|
||||
- **Никакого хардкода строк в компонентах.** Все тексты — в словарях ресурсов (ключ → значение),
|
||||
включая сообщения об ошибках, которые сейчас формируются на бэке (`{detail}`), — они должны быть
|
||||
локализуемы (ключ + параметры) или переводимы по коду.
|
||||
- **Переключение языка на лету**, без перезагрузки страницы; выбранный язык сохраняется (localStorage/настройки).
|
||||
- **Расширяемость:** добавление нового языка = новый файл словаря, без правок компонентов.
|
||||
- **Форматирование** дат/времени/чисел/валют — через i18n-форматтеры (не вручную), плюрализация —
|
||||
через правила языка.
|
||||
- Ключи — стабильные, сгруппированные по областям (nav/cards/settings/operator/…); отсутствующий
|
||||
ключ в языке → фолбэк на русский.
|
||||
- Бэк: ответы API остаются с `{detail}`/кодами; фронт отображает локализованный текст по коду/ключу
|
||||
(при необходимости — расширяемый словарь ошибок).
|
||||
|
||||
UI-область, к которой это применяется: основное приложение (дашборд, «Выбранные», настройки, каналы,
|
||||
обработка) и оператор-консоль (этап 10).
|
||||
|
||||
## Открытые точки согласования (накопились к концу этапа 1)
|
||||
|
||||
> Решения владельца (2026-09-06): 1 — бренд меняем (сделано точечно: index.html, LoginView, Sidebar, DiscoveryView, SettingsView); 2 — инвайты/оператор остаются на SaaS-этап, dev-seed admin/admin; 3 — PascalCase — конвенция БД; 4 — кука `deal_session` остаётся; 5 — заглушки сервисов допустимы (порты с детерминированными локальными реализациями до этапов 6+); 6 — идём по roadmap все этапы.
|
||||
|
||||
1. **Бренд во фронте**: Vue-фронт всё ещё показывает «LeadRadar» (LoginView, заголовки). Фронт
|
||||
«не трогаем» — но бренд теперь «Дейл». Менять ли строки бренда во фронте (точечно) или позже?
|
||||
2. **Инвайты/оператор**: ТЗ требует invite-only + отдельный вход оператора; во фронте такого UI нет.
|
||||
Оставляем dev-seed (admin/admin + дефолтный тенант) до этапа 7? Тогда auth остаётся «как прототип».
|
||||
3. **Имена колонок БД**: EF генерирует PascalCase (`UpdatedAt`), ТЗ/доки местами в SQL-нотации
|
||||
(snake_case). Оставляем PascalCase (конвенция кода) — подтвердить.
|
||||
4. **Имя куки** `deal_session` (прототип: `leadradar_session`) — фронту всё равно; ок?
|
||||
5. **Сервисы ml/ai/telegram**: полный ребилд — это самые крупные этапы. Подтвердить, что локально
|
||||
между этапами допустимы порты с детерминированными заглушками (пока сервисы не написаны), чтобы
|
||||
pipeline/kanban разрабатывать и показывать на синтетических входах.
|
||||
6. **Приоритет и темп**: продолжать строго по roadmap (Settings → Kanban → Pipeline → Projects →
|
||||
сервисы+Discovery → SaaS-контур) и в каком темпе/объёме на сессию.
|
||||
|
||||
**Статус на 2026-09-08 (этапы 0–7 завершены):** п.2 **закрыт** — инвайты/оператор реализованы в этапе 7
|
||||
(API-only: `/api/operator/*` + `/api/join`), dev-seed `admin/admin` остаётся dev-only (Ruling 1);
|
||||
UI операторской админки и страницы активации — задел этапа 8+. п.1 — бренд во фронте меняется точечно
|
||||
(как решено); п.3–5 — зафиксированы решениями (PascalCase, `deal_session`, локальные реализации портов)
|
||||
и не пересматривались; п.6 — roadmap исполнен: этапы 0–7 пройдены (STATUS.md — 100%, Manual-чек-лист
|
||||
вынесен отдельно).
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,136 +1,136 @@
|
||||
# Дейл (Deal) — Этап 1: Доступ, мультитенантность и фундамент данных Implementation Plan
|
||||
|
||||
> Исторический документ этапа 1. Актуальное состояние — `docs/superpowers/STATUS.md` и `docs/technical/Техническая-документация-Дейл.md`.
|
||||
|
||||
**Goal:** Сделать ядро «Дейл» способным принимать запросы реального фронта (Vue из LeadRadar):
|
||||
аутентификация (login/logout/me/change-password) на пользователях в `public`, сессии (httpOnly-cookie),
|
||||
tenant-контекст из сессии, физическая схема на тенанта с применением tenant-миграций, первая
|
||||
tenant-таблица (настройки). Этап закладывает паттерн для всех последующих модулей.
|
||||
|
||||
**Architecture:** модульный монолит `src/core`. HTTP-эндпоинты живут в `Deal.Api` (папка `Endpoints/`),
|
||||
вызывают сервисы модулей. Модули содержат домен и прикладные сервисы, НЕ содержат EF. Персистентность —
|
||||
в `Deal.Infrastructure` (два DbContext: системный `public` и tenant-схемы) + сущности/конфигурации
|
||||
модулей подключаются туда по одному соглашению (см. Ruling 1).
|
||||
|
||||
**Spec:** `docs/architecture/2026-09-05-deal-architecture-design.md` §4, 6.1, 8; `docs/spec/ТЗ-дейл-новая-архитектура.md` §3, 8 (частично); контракт: `docs/api/api-map.md` (auth); референс-семантика: `backend/app/auth.py`, `backend/app/routers/auth_routes.py`, `backend/app/main.py`, `backend/app/config.py`.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Проект **НЕ git**; фиксация — отчёты задач и `progress.md` плана. Рабочая папка плана: `.superpowers/sdd/deal-stage1-tenancy/`.
|
||||
- .NET 10 SDK, решение собирается с 0 warnings / 0 errors (TreatWarningsAsErrors).
|
||||
- Код-стайл: 1 тип = 1 файл; XML-doc для public-контрактов; комментарии на русском; явные модификаторы; настройки через `IOptions<T>`; без регионов и snake_case-хелперов.
|
||||
- namespace `Deal.*`. Секретов в коде нет (dev-пароль по умолчанию — только seed, из env `DEAL_BOOTSTRAP_*`).
|
||||
- Сущности тенантов — в схеме `tenant_<id>`; системные — в `public`. `tenantId` только из сессии, никогда из тела запроса.
|
||||
- LeadRadar-контейнеры и `backend/`, `mlservice/`, `src/frontend/` не трогаем. Dev-Postgres `deal-postgres` (:5433) — наша БД.
|
||||
|
||||
## Зафиксированные решения (Rulings этапа)
|
||||
|
||||
- **Ruling 1 (модель персистентности этапа):** сущности этапа 1 — в `Deal.Infrastructure/Persistence/Entities` (POCO, 1 тип = 1 файл), EF-конфигурации — в `Deal.Infrastructure/Persistence` (рядом с контекстами). Модули (`Deal.Modules.*`) НЕ содержат EF и НЕ ссылаются на Infrastructure: они объявляют интерфейсы своих хранилищ/сервисов и работают с record-DTO. Реализации интерфейсов — в Infrastructure (паттерн «port & adapter»). Это эталон для последующих модулей; когда у модуля появится богатая логика, его сущности переедут в модуль без изменения контрактов наружу.
|
||||
- **Ruling 2 (два контекста):** `DealDbContext` остаётся системным (схема `public`, явный `ToTable(...,"public")`;
|
||||
таблицы: tenants, users, sessions). Новый `TenantDbContext` — бессхемная модель (таблицы без указания схемы),
|
||||
живут в схеме через `search_path`. У `TenantDbContext` `MigrationsHistoryTable` получает ИМЯ
|
||||
`__TenantMigrationsHistory` и схему текущего тенанта на этапе применения (см. Ruling 3).
|
||||
- **Ruling 3 (применение tenant-миграций):** `TenantProvisioningService` для каждого тенанта: (1) создать схему
|
||||
`tenant_<id>` (SQL `TenantSchemaMigrator.CreateSchemaSql`), (2) открыть контекст на строке подключения с
|
||||
`Search Path=tenant_<id>` и `MigrationsHistoryTable("__TenantMigrationsHistory", "tenant_<id>")`, (3) `Database.Migrate()`.
|
||||
- **Ruling 4 (dev-сброс схемы):** в `public` уже применена `InitialPublic` (пустая таблица tenants — тестовые данные).
|
||||
Пересоздаём миграции системного контекста начисто: удаляем старую миграцию `InitialPublic`, создаём
|
||||
`InitialSystem` (tenants+users+sessions), дропаем и пересоздаём dev-БД (`deal-postgres`). Реальные данные отсутствуют.
|
||||
- **Ruling 5 (hash пароля):** Argon2id через пакет `Isopoh.Cryptography.Argon2` (чистый managed, без нативных
|
||||
зависимостей). Формат хранения — encoded-строка из `Argon2.Hash(password)`; проверка `Argon2.Verify`.
|
||||
- **Ruling 6 (сессии):** токен = 32 случайных байта (Base64Url); в БД хранится SHA-256 токена. Кука
|
||||
`deal_session`, httpOnly, SameSite=Lax, MaxAge=30 дней, `Secure` — из конфига (dev=false). Смена пароля
|
||||
удаляет все сессии пользователя и выдаёт свежую (семантика прототипа `auth.py`).
|
||||
- **Ruling 7 (эндпоинты):** минимальные API-эндпоинты живут в `Deal.Api/Endpoints/` (статик-классы `MapXxxEndpoints(this IEndpointRouteBuilder)`), делегируют в интерфейсы модулей. Конвенция для всех модулей.
|
||||
- **Ruling 7a (DTO модулей):** модуль объявляет record-DTO (папка `Application/Models`), сериализация наружу — camelCase (ASP.NET default); эндпоинты не видят EF-сущности.
|
||||
- **Ruling 8 (bootstrap/seed):** при старте, если нет тенантов: создаём дефолтного тенанта с ФИКСИРОВАННЫМ id `00000000-0000-0000-0000-000000000001` (схема `tenant_000...0001`, детерминирована) и пользователя `admin` (логин/пароль из env `DEAL_BOOTSTRAP_LOGIN/PASSWORD`, по умолчанию `admin`/`admin`) — повторяет `ensure_creds` прототипа. Seed идемпотентен. Провижининг схемы дефолтного тенанта — тем же `TenantProvisioningService`.
|
||||
- **Ruling 9 (первая tenant-таблица):** `settings` (модуль Settings): `key text PK`, `value_json text NOT NULL`,
|
||||
`updated_at timestamptz NOT NULL`. Без неё tenant-миграции нечего применять; таблица понадобится всем модулям.
|
||||
- **Ruling 10 (DTO/сериализация):** ответы — camelCase JSON (ASP.NET default); ошибки — HTTP-код + `{"detail": "..."}`
|
||||
(семантика FastAPI, см. `api.js`).
|
||||
|
||||
## Задачи
|
||||
|
||||
### Task 1: Карта API
|
||||
Выполнена (артефакт `docs/api/api-map.md`). В этом этапе используется секция Auth.
|
||||
|
||||
### Task 2: Персистентность — системный и tenant-контексты, миграции
|
||||
|
||||
**Files:**
|
||||
- Create: `src/core/Deal.Infrastructure/Persistence/TenantDbContext.cs`
|
||||
- Create: `src/core/Deal.Infrastructure/Persistence/Entities/UserEntity.cs`, `SessionEntity.cs`, `TenantSettingEntity.cs`
|
||||
- Create: `src/core/Deal.Infrastructure/Persistence/UserConfiguration.cs`, `SessionConfiguration.cs`, `TenantSettingConfiguration.cs`
|
||||
- Modify: `Deal.Infrastructure/Persistence/DealDbContext.cs` (добавить DbSet Users/Sessions)
|
||||
- Create: `src/core/Deal.Infrastructure/Persistence/TenantDbDesignTimeFactory.cs`
|
||||
- Delete: старая миграция `InitialPublic*` в `Deal.Infrastructure/Migrations/` (и `DealDbContextModelSnapshot.cs` — пересоздастся)
|
||||
- Migrations: `Migrations/InitialSystem` (контекст DealDbContext), `Migrations/InitialTenant` (контекст TenantDbContext) — обе в общей папке `Migrations/` (без `--output-dir`): имена классов миграций и снапшотов (`DealDbContextModelSnapshot`/`TenantDbContextModelSnapshot`) не конфликтуют.
|
||||
|
||||
**Acceptance:**
|
||||
1. `DealDbContext` (системный): `Tenants`, `Users`, `Sessions` в схеме `public` (явная схема в конфигурациях).
|
||||
2. `TenantDbContext`: модель без схемы, таблица `settings` (см. Ruling 9), `MigrationsHistoryTable` = `__TenantMigrationsHistory` (без схемы в модели; схема задаётся при применении).
|
||||
3. Сущности — в отдельных файлах (1 тип = 1 файл), конфигурации в отдельных файлах.
|
||||
4. Сборка: `dotnet build Deal.sln` — 0 warnings/0 errors.
|
||||
5. Dev-БД пересоздана: `public` содержит `tenants`, `users`, `sessions`, `__EFMigrationsHistory` (одна строка `InitialSystem`).
|
||||
6. Tenant-миграция `InitialTenant` существует и при применении к схеме создаёт там `settings` и историю — проверка через psql (применение выполняет Task 5; здесь достаточно `dotnet ef migrations list` и того, что SQL миграции не содержит схемы).
|
||||
7. Отчёт: `task-2-report.md`.
|
||||
|
||||
### Task 3: Модуль Tenants — домен и прикладные сервисы аутентификации
|
||||
|
||||
**Files:**
|
||||
- Create: `src/core/Deal.Modules.Tenants/Application/IPasswordHasher.cs`, `DefaultPasswordHasher.cs` (Argon2id, Ruling 5)
|
||||
- Create: `src/core/Deal.Modules.Tenants/Application/Models/*.cs` — record-DTO: `UserIdentityDto`, `SessionDto`, `LoginResult` и т.п. (минимум, что нужно сервисам)
|
||||
- Create: `src/core/Deal.Modules.Tenants/Application/IAuthStore.cs` (поиск пользователя по логину, чтение/создание/удаление сессий, смена пароля — на DTO)
|
||||
- Create: `src/core/Deal.Modules.Tenants/Application/AuthService.cs` (login/logout/changePassword/resolveSession)
|
||||
- Create: `src/core/Deal.Modules.Tenants/Application/ITenantRepository.cs`, `TenantService.cs` (реестр тенантов; создание тенанта вызывает `ITenantProvisioner` — интерфейс из модуля)
|
||||
- Modify: `Deal.Infrastructure` — EF-реализации (`Persistence/Repositories/AuthStore.cs`, `TenantRepository.cs`) + регистрация DI (`Deal.Infrastructure/ServiceCollectionExtensions.cs`)
|
||||
- Test: `tests/Deal.Tests.Unit/PasswordHasherTests.cs`, `AuthServiceTests.cs` (с fake-хранилищем)
|
||||
|
||||
**Семантика (референс `backend/app/auth.py`):**
|
||||
- login: неверные данные → 401 «Неверный логин или пароль»; ok → `{ok:true, login}`.
|
||||
- changePassword: `oldPassword` неверен → false→400 «Текущий пароль неверен»; новая длина <4 → 400 «Пароль слишком короткий (минимум 4 символа)»; успех → удалить все сессии пользователя.
|
||||
- resolveSession по токену (с учётом expires) → login.
|
||||
- Сессия живёт 30 дней; «протухшие» сессии удаляются при resolve (очистка).
|
||||
|
||||
**Acceptance:** build 0/0; `dotnet test tests/Deal.Tests.Unit` — все PASS (было 6 + новые ≥6). Тесты: hash/verify, неверный пароль, смена пароля инвалидирует старые сессии, resolve протухшей сессии → null. Отчёт: `task-3-report.md`.
|
||||
|
||||
### Task 4: Эндпоинты auth, middleware сессии, DI, curl-приёмка
|
||||
|
||||
**Files:**
|
||||
- Create: `src/core/Deal.Api/Endpoints/AuthEndpoints.cs`
|
||||
- Create: `src/core/Deal.Api/Middleware/SessionMiddleware.cs` (чтение куки → resolve → `TenantContext` + `CurrentUser` в `HttpContext.Items`; слабые запросы без сессии — дальше, 401 выставляют сами эндпоинты)
|
||||
- Create: `src/core/Deal.Api/Configuration/CookieOptions.cs` (IOptions; Name=deal_session, Days=30, Secure=false)
|
||||
- Modify: `Deal.Api/Program.cs` (CORS dev как в прототипе, cookie-конфиг, DI модулей+инфраструктуры, map auth-группы; статика SPA не нужна)
|
||||
- Test/скрипт приёмки: последовательность curl на :5080 (health → login admin/admin → cookie → me → change-password → старый logout/401)
|
||||
|
||||
**Контракт эндпоинтов (1:1 с прототипом):** `POST /api/auth/login` {login,password} → 200 {ok,login} | 401; `POST /api/auth/logout` → {ok:true}; `GET /api/auth/me` → 200 {login,ok} | 401 {detail:"Требуется авторизация"}; `POST /api/auth/change-password` {oldPassword,newPassword} → {ok:true} | 400.
|
||||
|
||||
**Acceptance:** build 0/0; curl-цепочка проходит (кука выставляется, me работает, после logout — 401). Отчёт: `task-4-report.md`.
|
||||
|
||||
### Task 5: Провижининг схем тенантов и bootstrap при старте
|
||||
|
||||
**Files:**
|
||||
- Create: `src/core/Deal.Infrastructure/Tenancy/TenantProvisioningService.cs` (Ruling 3; реализует `ITenantProvisioner` из модуля)
|
||||
- Create: `src/core/Deal.Api/Hosting/TenantBootstrapService.cs` (IHostedService: seed дефолтного тенанта+admin (Ruling 8), провижининг схем ВСЕХ тенантов при старте; идемпотентно)
|
||||
- Modify: `Deal.Modules.Tenants/Application/IAuthStore.cs` — добавить `Task CreateUserAsync(StoredUserDto user, CancellationToken ct)` (seed через порт модуля, НЕ через DbContext в Api)
|
||||
- Modify: `Deal.Infrastructure/Persistence/Repositories/AuthStore.cs` — реализовать CreateUserAsync
|
||||
- Modify: `Deal.Modules.Tenants/Application/TenantService.cs` — `CreateTenantAsync(string name, CancellationToken)` оставить; при необходимости дать возможность передать явный Guid id (для дефолтного тенанта)
|
||||
- Modify: `Deal.Infrastructure/ServiceCollectionExtensions.cs` — регистрация `ITenantProvisioner→TenantProvisioningService`
|
||||
- Modify: `Deal.Api/Program.cs` — hosted-сервис вместо StartupSeed; удалить `PendingTenantProvisioner`
|
||||
- Delete: `Deal.Api/Hosting/StartupSeed.cs`, временная DI-заглушка `PendingTenantProvisioner`
|
||||
- Modify: `Deal.Api/Configuration/CookieOptions.cs` — `Days` по умолчанию = константа сессии модуля (единый источник «30»)
|
||||
|
||||
**Acceptance:** app стартует, seed создан (psql: tenants строка с фикс. id, users `admin`), схема `tenant_<32hex>` дефолтного тенанта создана с таблицей `settings` и `__TenantMigrationsHistory` (содержит InitialTenant); повторный старт идемпотентен; `dotnet build` 0/0; все тесты PASS; login admin/admin работает после старта. Отчёт: `task-5-report.md`.
|
||||
|
||||
### Task 6: Финал этапа
|
||||
|
||||
- `scripts/build.sh`, `scripts/test.sh` — успешны; `dotnet ef migrations list` — System: InitialSystem, Tenant: InitialTenant.
|
||||
- Полная curl-приёмка (health, login, me, logout) + psql-проверка схем.
|
||||
- Обновить `docs/technical/Техническая-документация-Дейл.md` (раздел «Быстрый старт dev» — актуальные шаги: поднять postgres, мигрировать public, запустить API, креды).
|
||||
- Отчёт `task-6-report.md` + финальная строка в `progress.md`.
|
||||
|
||||
## Self-Review
|
||||
|
||||
1. Spec coverage: ТЗ §3 (роли/доступ) — Task 3–5; архитектура §4 (мультитенантность) — Task 2, 5; §6.1 (контракт /api, auth) — Task 4; §11 (стандарты) — все задачи.
|
||||
2. Placeholder scan: код везде конкретный; референсы на `auth.py`/api-map точные.
|
||||
3. Type consistency: `TenantId`, `ITenantContext`, `TenantContext`, `ConnectionStringProvider`, `TenantProvisioningService`, `DealDbContext`, `TenantDbContext`, сущности — согласованы между задачами 2–5.
|
||||
4. Вне scope этапа 1: kanban/колонки/карточки, проекты, pipeline/очередь/отсев, discovery, сервисы ml/ai/telegram, операторская админка, инвайты, лимиты токенов, валюты — следующие этапы.
|
||||
# Дейл (Deal) — Этап 1: Доступ, мультитенантность и фундамент данных Implementation Plan
|
||||
|
||||
> Исторический документ этапа 1. Актуальное состояние — `docs/superpowers/STATUS.md` и `docs/technical/Техническая-документация-Дейл.md`.
|
||||
|
||||
**Goal:** Сделать ядро «Дейл» способным принимать запросы реального фронта (Vue из LeadRadar):
|
||||
аутентификация (login/logout/me/change-password) на пользователях в `public`, сессии (httpOnly-cookie),
|
||||
tenant-контекст из сессии, физическая схема на тенанта с применением tenant-миграций, первая
|
||||
tenant-таблица (настройки). Этап закладывает паттерн для всех последующих модулей.
|
||||
|
||||
**Architecture:** модульный монолит `src/core`. HTTP-эндпоинты живут в `Deal.Api` (папка `Endpoints/`),
|
||||
вызывают сервисы модулей. Модули содержат домен и прикладные сервисы, НЕ содержат EF. Персистентность —
|
||||
в `Deal.Infrastructure` (два DbContext: системный `public` и tenant-схемы) + сущности/конфигурации
|
||||
модулей подключаются туда по одному соглашению (см. Ruling 1).
|
||||
|
||||
**Spec:** `docs/architecture/2026-09-05-deal-architecture-design.md` §4, 6.1, 8; `docs/spec/ТЗ-дейл-новая-архитектура.md` §3, 8 (частично); контракт: `docs/api/api-map.md` (auth); референс-семантика: `backend/app/auth.py`, `backend/app/routers/auth_routes.py`, `backend/app/main.py`, `backend/app/config.py`.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Проект **НЕ git**; фиксация — отчёты задач и `progress.md` плана. Рабочая папка плана: `.superpowers/sdd/deal-stage1-tenancy/`.
|
||||
- .NET 10 SDK, решение собирается с 0 warnings / 0 errors (TreatWarningsAsErrors).
|
||||
- Код-стайл: 1 тип = 1 файл; XML-doc для public-контрактов; комментарии на русском; явные модификаторы; настройки через `IOptions<T>`; без регионов и snake_case-хелперов.
|
||||
- namespace `Deal.*`. Секретов в коде нет (dev-пароль по умолчанию — только seed, из env `DEAL_BOOTSTRAP_*`).
|
||||
- Сущности тенантов — в схеме `tenant_<id>`; системные — в `public`. `tenantId` только из сессии, никогда из тела запроса.
|
||||
- LeadRadar-контейнеры и `backend/`, `mlservice/`, `src/frontend/` не трогаем. Dev-Postgres `deal-postgres` (:5433) — наша БД.
|
||||
|
||||
## Зафиксированные решения (Rulings этапа)
|
||||
|
||||
- **Ruling 1 (модель персистентности этапа):** сущности этапа 1 — в `Deal.Infrastructure/Persistence/Entities` (POCO, 1 тип = 1 файл), EF-конфигурации — в `Deal.Infrastructure/Persistence` (рядом с контекстами). Модули (`Deal.Modules.*`) НЕ содержат EF и НЕ ссылаются на Infrastructure: они объявляют интерфейсы своих хранилищ/сервисов и работают с record-DTO. Реализации интерфейсов — в Infrastructure (паттерн «port & adapter»). Это эталон для последующих модулей; когда у модуля появится богатая логика, его сущности переедут в модуль без изменения контрактов наружу.
|
||||
- **Ruling 2 (два контекста):** `DealDbContext` остаётся системным (схема `public`, явный `ToTable(...,"public")`;
|
||||
таблицы: tenants, users, sessions). Новый `TenantDbContext` — бессхемная модель (таблицы без указания схемы),
|
||||
живут в схеме через `search_path`. У `TenantDbContext` `MigrationsHistoryTable` получает ИМЯ
|
||||
`__TenantMigrationsHistory` и схему текущего тенанта на этапе применения (см. Ruling 3).
|
||||
- **Ruling 3 (применение tenant-миграций):** `TenantProvisioningService` для каждого тенанта: (1) создать схему
|
||||
`tenant_<id>` (SQL `TenantSchemaMigrator.CreateSchemaSql`), (2) открыть контекст на строке подключения с
|
||||
`Search Path=tenant_<id>` и `MigrationsHistoryTable("__TenantMigrationsHistory", "tenant_<id>")`, (3) `Database.Migrate()`.
|
||||
- **Ruling 4 (dev-сброс схемы):** в `public` уже применена `InitialPublic` (пустая таблица tenants — тестовые данные).
|
||||
Пересоздаём миграции системного контекста начисто: удаляем старую миграцию `InitialPublic`, создаём
|
||||
`InitialSystem` (tenants+users+sessions), дропаем и пересоздаём dev-БД (`deal-postgres`). Реальные данные отсутствуют.
|
||||
- **Ruling 5 (hash пароля):** Argon2id через пакет `Isopoh.Cryptography.Argon2` (чистый managed, без нативных
|
||||
зависимостей). Формат хранения — encoded-строка из `Argon2.Hash(password)`; проверка `Argon2.Verify`.
|
||||
- **Ruling 6 (сессии):** токен = 32 случайных байта (Base64Url); в БД хранится SHA-256 токена. Кука
|
||||
`deal_session`, httpOnly, SameSite=Lax, MaxAge=30 дней, `Secure` — из конфига (dev=false). Смена пароля
|
||||
удаляет все сессии пользователя и выдаёт свежую (семантика прототипа `auth.py`).
|
||||
- **Ruling 7 (эндпоинты):** минимальные API-эндпоинты живут в `Deal.Api/Endpoints/` (статик-классы `MapXxxEndpoints(this IEndpointRouteBuilder)`), делегируют в интерфейсы модулей. Конвенция для всех модулей.
|
||||
- **Ruling 7a (DTO модулей):** модуль объявляет record-DTO (папка `Application/Models`), сериализация наружу — camelCase (ASP.NET default); эндпоинты не видят EF-сущности.
|
||||
- **Ruling 8 (bootstrap/seed):** при старте, если нет тенантов: создаём дефолтного тенанта с ФИКСИРОВАННЫМ id `00000000-0000-0000-0000-000000000001` (схема `tenant_000...0001`, детерминирована) и пользователя `admin` (логин/пароль из env `DEAL_BOOTSTRAP_LOGIN/PASSWORD`, по умолчанию `admin`/`admin`) — повторяет `ensure_creds` прототипа. Seed идемпотентен. Провижининг схемы дефолтного тенанта — тем же `TenantProvisioningService`.
|
||||
- **Ruling 9 (первая tenant-таблица):** `settings` (модуль Settings): `key text PK`, `value_json text NOT NULL`,
|
||||
`updated_at timestamptz NOT NULL`. Без неё tenant-миграции нечего применять; таблица понадобится всем модулям.
|
||||
- **Ruling 10 (DTO/сериализация):** ответы — camelCase JSON (ASP.NET default); ошибки — HTTP-код + `{"detail": "..."}`
|
||||
(семантика FastAPI, см. `api.js`).
|
||||
|
||||
## Задачи
|
||||
|
||||
### Task 1: Карта API
|
||||
Выполнена (артефакт `docs/api/api-map.md`). В этом этапе используется секция Auth.
|
||||
|
||||
### Task 2: Персистентность — системный и tenant-контексты, миграции
|
||||
|
||||
**Files:**
|
||||
- Create: `src/core/Deal.Infrastructure/Persistence/TenantDbContext.cs`
|
||||
- Create: `src/core/Deal.Infrastructure/Persistence/Entities/UserEntity.cs`, `SessionEntity.cs`, `TenantSettingEntity.cs`
|
||||
- Create: `src/core/Deal.Infrastructure/Persistence/UserConfiguration.cs`, `SessionConfiguration.cs`, `TenantSettingConfiguration.cs`
|
||||
- Modify: `Deal.Infrastructure/Persistence/DealDbContext.cs` (добавить DbSet Users/Sessions)
|
||||
- Create: `src/core/Deal.Infrastructure/Persistence/TenantDbDesignTimeFactory.cs`
|
||||
- Delete: старая миграция `InitialPublic*` в `Deal.Infrastructure/Migrations/` (и `DealDbContextModelSnapshot.cs` — пересоздастся)
|
||||
- Migrations: `Migrations/InitialSystem` (контекст DealDbContext), `Migrations/InitialTenant` (контекст TenantDbContext) — обе в общей папке `Migrations/` (без `--output-dir`): имена классов миграций и снапшотов (`DealDbContextModelSnapshot`/`TenantDbContextModelSnapshot`) не конфликтуют.
|
||||
|
||||
**Acceptance:**
|
||||
1. `DealDbContext` (системный): `Tenants`, `Users`, `Sessions` в схеме `public` (явная схема в конфигурациях).
|
||||
2. `TenantDbContext`: модель без схемы, таблица `settings` (см. Ruling 9), `MigrationsHistoryTable` = `__TenantMigrationsHistory` (без схемы в модели; схема задаётся при применении).
|
||||
3. Сущности — в отдельных файлах (1 тип = 1 файл), конфигурации в отдельных файлах.
|
||||
4. Сборка: `dotnet build Deal.sln` — 0 warnings/0 errors.
|
||||
5. Dev-БД пересоздана: `public` содержит `tenants`, `users`, `sessions`, `__EFMigrationsHistory` (одна строка `InitialSystem`).
|
||||
6. Tenant-миграция `InitialTenant` существует и при применении к схеме создаёт там `settings` и историю — проверка через psql (применение выполняет Task 5; здесь достаточно `dotnet ef migrations list` и того, что SQL миграции не содержит схемы).
|
||||
7. Отчёт: `task-2-report.md`.
|
||||
|
||||
### Task 3: Модуль Tenants — домен и прикладные сервисы аутентификации
|
||||
|
||||
**Files:**
|
||||
- Create: `src/core/Deal.Modules.Tenants/Application/IPasswordHasher.cs`, `DefaultPasswordHasher.cs` (Argon2id, Ruling 5)
|
||||
- Create: `src/core/Deal.Modules.Tenants/Application/Models/*.cs` — record-DTO: `UserIdentityDto`, `SessionDto`, `LoginResult` и т.п. (минимум, что нужно сервисам)
|
||||
- Create: `src/core/Deal.Modules.Tenants/Application/IAuthStore.cs` (поиск пользователя по логину, чтение/создание/удаление сессий, смена пароля — на DTO)
|
||||
- Create: `src/core/Deal.Modules.Tenants/Application/AuthService.cs` (login/logout/changePassword/resolveSession)
|
||||
- Create: `src/core/Deal.Modules.Tenants/Application/ITenantRepository.cs`, `TenantService.cs` (реестр тенантов; создание тенанта вызывает `ITenantProvisioner` — интерфейс из модуля)
|
||||
- Modify: `Deal.Infrastructure` — EF-реализации (`Persistence/Repositories/AuthStore.cs`, `TenantRepository.cs`) + регистрация DI (`Deal.Infrastructure/ServiceCollectionExtensions.cs`)
|
||||
- Test: `tests/Deal.Tests.Unit/PasswordHasherTests.cs`, `AuthServiceTests.cs` (с fake-хранилищем)
|
||||
|
||||
**Семантика (референс `backend/app/auth.py`):**
|
||||
- login: неверные данные → 401 «Неверный логин или пароль»; ok → `{ok:true, login}`.
|
||||
- changePassword: `oldPassword` неверен → false→400 «Текущий пароль неверен»; новая длина <4 → 400 «Пароль слишком короткий (минимум 4 символа)»; успех → удалить все сессии пользователя.
|
||||
- resolveSession по токену (с учётом expires) → login.
|
||||
- Сессия живёт 30 дней; «протухшие» сессии удаляются при resolve (очистка).
|
||||
|
||||
**Acceptance:** build 0/0; `dotnet test tests/Deal.Tests.Unit` — все PASS (было 6 + новые ≥6). Тесты: hash/verify, неверный пароль, смена пароля инвалидирует старые сессии, resolve протухшей сессии → null. Отчёт: `task-3-report.md`.
|
||||
|
||||
### Task 4: Эндпоинты auth, middleware сессии, DI, curl-приёмка
|
||||
|
||||
**Files:**
|
||||
- Create: `src/core/Deal.Api/Endpoints/AuthEndpoints.cs`
|
||||
- Create: `src/core/Deal.Api/Middleware/SessionMiddleware.cs` (чтение куки → resolve → `TenantContext` + `CurrentUser` в `HttpContext.Items`; слабые запросы без сессии — дальше, 401 выставляют сами эндпоинты)
|
||||
- Create: `src/core/Deal.Api/Configuration/CookieOptions.cs` (IOptions; Name=deal_session, Days=30, Secure=false)
|
||||
- Modify: `Deal.Api/Program.cs` (CORS dev как в прототипе, cookie-конфиг, DI модулей+инфраструктуры, map auth-группы; статика SPA не нужна)
|
||||
- Test/скрипт приёмки: последовательность curl на :5080 (health → login admin/admin → cookie → me → change-password → старый logout/401)
|
||||
|
||||
**Контракт эндпоинтов (1:1 с прототипом):** `POST /api/auth/login` {login,password} → 200 {ok,login} | 401; `POST /api/auth/logout` → {ok:true}; `GET /api/auth/me` → 200 {login,ok} | 401 {detail:"Требуется авторизация"}; `POST /api/auth/change-password` {oldPassword,newPassword} → {ok:true} | 400.
|
||||
|
||||
**Acceptance:** build 0/0; curl-цепочка проходит (кука выставляется, me работает, после logout — 401). Отчёт: `task-4-report.md`.
|
||||
|
||||
### Task 5: Провижининг схем тенантов и bootstrap при старте
|
||||
|
||||
**Files:**
|
||||
- Create: `src/core/Deal.Infrastructure/Tenancy/TenantProvisioningService.cs` (Ruling 3; реализует `ITenantProvisioner` из модуля)
|
||||
- Create: `src/core/Deal.Api/Hosting/TenantBootstrapService.cs` (IHostedService: seed дефолтного тенанта+admin (Ruling 8), провижининг схем ВСЕХ тенантов при старте; идемпотентно)
|
||||
- Modify: `Deal.Modules.Tenants/Application/IAuthStore.cs` — добавить `Task CreateUserAsync(StoredUserDto user, CancellationToken ct)` (seed через порт модуля, НЕ через DbContext в Api)
|
||||
- Modify: `Deal.Infrastructure/Persistence/Repositories/AuthStore.cs` — реализовать CreateUserAsync
|
||||
- Modify: `Deal.Modules.Tenants/Application/TenantService.cs` — `CreateTenantAsync(string name, CancellationToken)` оставить; при необходимости дать возможность передать явный Guid id (для дефолтного тенанта)
|
||||
- Modify: `Deal.Infrastructure/ServiceCollectionExtensions.cs` — регистрация `ITenantProvisioner→TenantProvisioningService`
|
||||
- Modify: `Deal.Api/Program.cs` — hosted-сервис вместо StartupSeed; удалить `PendingTenantProvisioner`
|
||||
- Delete: `Deal.Api/Hosting/StartupSeed.cs`, временная DI-заглушка `PendingTenantProvisioner`
|
||||
- Modify: `Deal.Api/Configuration/CookieOptions.cs` — `Days` по умолчанию = константа сессии модуля (единый источник «30»)
|
||||
|
||||
**Acceptance:** app стартует, seed создан (psql: tenants строка с фикс. id, users `admin`), схема `tenant_<32hex>` дефолтного тенанта создана с таблицей `settings` и `__TenantMigrationsHistory` (содержит InitialTenant); повторный старт идемпотентен; `dotnet build` 0/0; все тесты PASS; login admin/admin работает после старта. Отчёт: `task-5-report.md`.
|
||||
|
||||
### Task 6: Финал этапа
|
||||
|
||||
- `scripts/build.sh`, `scripts/test.sh` — успешны; `dotnet ef migrations list` — System: InitialSystem, Tenant: InitialTenant.
|
||||
- Полная curl-приёмка (health, login, me, logout) + psql-проверка схем.
|
||||
- Обновить `docs/technical/Техническая-документация-Дейл.md` (раздел «Быстрый старт dev» — актуальные шаги: поднять postgres, мигрировать public, запустить API, креды).
|
||||
- Отчёт `task-6-report.md` + финальная строка в `progress.md`.
|
||||
|
||||
## Self-Review
|
||||
|
||||
1. Spec coverage: ТЗ §3 (роли/доступ) — Task 3–5; архитектура §4 (мультитенантность) — Task 2, 5; §6.1 (контракт /api, auth) — Task 4; §11 (стандарты) — все задачи.
|
||||
2. Placeholder scan: код везде конкретный; референсы на `auth.py`/api-map точные.
|
||||
3. Type consistency: `TenantId`, `ITenantContext`, `TenantContext`, `ConnectionStringProvider`, `TenantProvisioningService`, `DealDbContext`, `TenantDbContext`, сущности — согласованы между задачами 2–5.
|
||||
4. Вне scope этапа 1: kanban/колонки/карточки, проекты, pipeline/очередь/отсев, discovery, сервисы ml/ai/telegram, операторская админка, инвайты, лимиты токенов, валюты — следующие этапы.
|
||||
|
||||
@@ -1,430 +1,430 @@
|
||||
# Дейл (Deal) — Этап 2: Настройки тенанта (Settings) Implementation Plan
|
||||
|
||||
> Исторический документ этапа 2. Актуальное состояние — `docs/superpowers/STATUS.md` и `docs/technical/Техническая-документация-Дейл.md`.
|
||||
|
||||
**Goal:** Реализовать в модульном монолите `src/core` модуль Settings с 1:1-контрактом `/api`,
|
||||
который потребляет экран «Настройки» Vue-фронта (`src/frontend/src/views/SettingsView.vue`,
|
||||
`components/MLPanel.vue`, `PromptLibraryModal.vue`): чтение/сохранение дерева настроек тенанта
|
||||
(таблица `settings` уже есть), шифрование секретов (ключи AI/Telegram), проверка подключения
|
||||
AI-провайдера, курсы валют, ML-панель на детерминированной локальной заглушке, тестер фильтров
|
||||
входящих. К концу этапа Settings-экран обслуживается бэкендом полностью (кроме зон, помеченных
|
||||
зависимостями этапов 3–6); приёмка — curl/psql/unit-тесты (Vue-фронт полностью оживает только
|
||||
с этапом 3: его `boot()` требует `/api/boards`, `/api/leads`, `/api/projects`, `/api/tg/status` —
|
||||
см. Ruling 11).
|
||||
|
||||
**Architecture:** новый модуль `Deal.Modules.Settings` (чистый, без EF): константы/дефолты,
|
||||
типизированный каталог ключей, порты `ISettingsStore`/`ISecretCipher`/`IRatesSource`/
|
||||
`IAiConnectionChecker`, сервисы `SettingsService` (public-снимок + частичный PATCH), `RatesService`,
|
||||
`IncomingRules` (этап-1 правила тестера). Адаптеры — в `Deal.Infrastructure`: KV `SettingsStore`
|
||||
(таблица `settings`, JSON в `value_json`), `AesGcmSecretCipher`, `CbrRateSource`, HTTP-проверка AI.
|
||||
Интеграционный порт `IMlClient` + record-DTO — в `Deal.Contracts/Integrations`, заглушка
|
||||
`LocalMlClient` — в `Deal.Infrastructure/Integrations`. HTTP-эндпоинты — в `Deal.Api/Endpoints/`
|
||||
(`MapSettingsEndpoints`, `MapMlEndpoints`, `MapFilterTesterEndpoints`). Внешние сервисы
|
||||
(реальные ml/ai/telegram) на этапе 6 заменят заглушки gRPC-адаптерами без правки эндпоинтов.
|
||||
|
||||
**Spec:** `docs/api/api-map.md` §3.4 (L142–152), §3.7 (L187–199), §4.6 (L315–341), §4.7 (L343–346),
|
||||
§4.10 (L363–365), правила L7–24, п.9 «экономия» (L399); `docs/spec/ТЗ-дейл-новая-архитектура.md`
|
||||
§8 (L165–179), §5 (L89–121, фильтры), §7 (L150–161 — только пересечения), §9 (лимиты — НЕ в этап);
|
||||
`docs/architecture/2026-09-05-deal-architecture-design.md` §5 (границы модулей), §8 (секреты L207);
|
||||
референс-семантика: `backend/app/routers/settings_routes.py`, `backend/app/services/rates.py`,
|
||||
`backend/app/services/ai.py` (L36–77, L188–198), `backend/app/routers/ml_routes.py`,
|
||||
`backend/app/services/ml_client.py`, `backend/app/routers/dashboard_routes.py` (admin/check-message
|
||||
L267–284), `backend/app/services/pipeline.py` (stage1_plain L94–124), `backend/app/constants.py`
|
||||
(L30–50, L54–245), `backend/app/crypto.py`, `backend/app/config.py` (L48–51);
|
||||
фронт: `src/frontend/src/store.js` (boot L565–628, applySettings L343–397, applyMlStatus L487–502,
|
||||
schedulePersist L1737–1766, refreshRates L1844–1848), `src/frontend/src/data.js` (L6–141 дефолты
|
||||
промптов; `AI_PROVIDERS` L17–80; `PROMPT_LIBRARY` L180–200 — библиотека по сферам живёт ТОЛЬКО
|
||||
во фронте, бэкенд её не отдаёт), `views/SettingsView.vue` (вкладки L39–49), `components/MLPanel.vue`,
|
||||
`components/PromptLibraryModal.vue`.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Проект **НЕ git**; фиксация — отчёты задач `task-N-report.md` и `progress.md`. Рабочая папка плана: `.superpowers/sdd/deal-stage2-settings/`.
|
||||
- .NET 10 SDK, решение собирается с 0 warnings / 0 errors (`TreatWarningsAsErrors`).
|
||||
- Код-стайл этапа 1: 1 тип = 1 файл; XML-doc на public-контракты; комментарии на русском; явные модификаторы; настройки через `IOptions<T>`; без регионов.
|
||||
- namespace `Deal.*`. Секретов в коде нет; ключи шифрования — env/файл (Ruling 2). `tenantId` — только из сессии.
|
||||
- Таблица `settings` уже в `TenantDbContext` (миграция `InitialTenant`) — новые EF-таблицы в этапе 2 НЕ создаются.
|
||||
- LeadRadar-контейнеры, `backend/`, `mlservice/`, `src/frontend/` не трогаем. Dev-Postgres `deal-postgres` (:5433).
|
||||
- Ответы: camelCase JSON; ошибки — HTTP + `{"detail"}`; «мягкие» ошибки (ml/reset) — HTTP 200 с полем `error`.
|
||||
- Дефолтные значения настроек/промптов — из констант прототипа `constants.py` и `data.js` (фронт — высший авторитет форм; тексты промптов копируются из `data.js` L94–141).
|
||||
|
||||
## Зафиксированные решения (Rulings этапа)
|
||||
|
||||
- **Ruling 1 (модель настроек):** типизированные ключи в существующей таблице `settings`
|
||||
(`key` text PK, `value_json` — JSON-сериализованное значение любого типа, `updated_at`).
|
||||
Модуль хранит только переопределения; дефолты — в коде (`SettingsDefaults`), при чтении
|
||||
снимок = дефолты, перекрытые сохранёнными значениями. Каталог публичных ключей —
|
||||
статический словарь «ключ → категория» (Int/Bool/String/List/Dict/MyPrompts/AiConfigs/TgKeys).
|
||||
Внутренние (непубличные) ключи — `ratesCache`, `mlDecisions`, `aiDecisions` — хранятся в той же
|
||||
таблице через `ISettingsStore`, но в GET/PATCH `/settings` не участвуют. Неизвестные ключи в
|
||||
PATCH игнорируются (семантика `settings_routes.py` L110–185).
|
||||
- **Ruling 2 (шифрование секретов):** AES-256-GCM (`System.Security.Cryptography.AesGcm`),
|
||||
nonce 12 байт, tag 16 байт. Ключ — env `DEAL_ENCRYPTION_KEY` (32 байта, urlsafe-Base64);
|
||||
при отсутствии в dev — файл `<ContentRoot>/data/encryption.key` (генерируется при первом
|
||||
старте, лог-warning; путь переопределяется env `DEAL_ENCRYPTION_KEY_FILE`). Формат значения в
|
||||
БД: `enc:` + Base64(nonce‖ct‖tag). Расшифровка повреждённого/чужого значения → пустая строка
|
||||
+ warning (совместимость `crypto.decrypt_text`, `crypto.py` L52–61). Порт `ISecretCipher` —
|
||||
в модуле Settings, адаптер `AesGcmSecretCipher` — в Infrastructure.
|
||||
- **Ruling 3 (маски и публичная форма):** маска `mask(v)`: пусто → `""`, `len≤8` → как есть,
|
||||
иначе `v[:4]+"…"+v[-4:]` (`settings_routes.py` L28–32). `aiConfigs` наружу —
|
||||
`{id: {baseUrl, model, keySet, keyMasked}}`; `tgKeys` — `{apiId: <маска>, apiHashSet: bool}`.
|
||||
Список `providers` — статический из модуля (`id,name,base,local,models`; зеркало
|
||||
`constants.AI_PROVIDERS` L170–186; `api_style` — внутреннее поле, наружу не отдаётся).
|
||||
- **Ruling 4 (границы интеграционных портов):** порты будущих внешних сервисов (ML/AI/telegram)
|
||||
объявляются в `Deal.Contracts/Integrations` (интерфейс + record-DTO) — их потребляют несколько
|
||||
модулей и Api. Заглушки этапа — детерминированные адаптеры в `Deal.Infrastructure/Integrations`;
|
||||
на этапе 6 заменяются gRPC-клиентами с тем же контрактом. `IAiFacade` на этапе 2 не заводится:
|
||||
классификация/фильтр ИИ — этап 6, проверка соединения — модульный порт `IAiConnectionChecker`.
|
||||
- **Ruling 5 (ML-заглушка):** `IMlClient` (Contracts): `StatusAsync/PredictAsync/ResetAsync`
|
||||
(+ `PushAsync` добавится этапом 3). `LocalMlClient` — детерминированная: `reachable=true`,
|
||||
`ready=false`, `classes={}`, `learned=0`, `eval={count:0,correct:0,accuracy:0}` (обучение на
|
||||
действиях появится с Kanban-этапом 3); `PredictAsync` неготовой модели →
|
||||
`{take:false,label:null,scores:{},hits:0,ready:false,margin:null,terms:[],type:null}`;
|
||||
`ResetAsync` → `{ok:true}`. Таблиц `ml_outbox`/`learning_log` в этапе 2 нет (владельцы —
|
||||
этапы 3/4); счётчики `mlDecisions`/`aiDecisions` — KV-настройки.
|
||||
- **Ruling 6 (курсы валют):** кэш — tenant-настройка `ratesCache` `{rates, source, updatedAtMs}`.
|
||||
Источник по `rateSource` (`cbr`|`mock`); интервал обновления 6 часов (≤4 запроса/сутки,
|
||||
`rates.py` L20); `USDT=USD` (`rates.py` L86–91). Mock-курсы — константа `MockRates`
|
||||
(`constants.py` L41–50). Обновление: лениво на GET при протухании/смене источника, синхронно
|
||||
на `POST /rates/refresh`, фоново-запуск на PATCH `rateSource` (`settings_routes.py` L186–192).
|
||||
Массовый пересчёт карточек (`recompute_conversions`) — этап 3 (таблицы leads нет); в этапе 2 —
|
||||
только чистый `ConvertAmount`.
|
||||
- **Ruling 7 (проверка AI):** реальный HTTP, без LLM-вызовов, 1:1 `settings_routes.py` L195–219:
|
||||
нет ключа → `{ok:false, message:"Не задан API-ключ"}`; локальный провайдер → `{ok:true,
|
||||
message:"Локальный сервер «<name>» (ping в проде)"}`; облачный → `GET {base}/models`
|
||||
(Anthropic: `{base}/v1/models`, заголовок `x-api-key`); HTTP<400 → ok, 401/403 → «Ключ не
|
||||
принят (HTTP n)…», иначе «HTTP n — проверьте Base URL и модель»; сетевой сбой → «Ошибка
|
||||
соединения: …». Ответ — `{ok, message}` + статус провайдера (`provider,name,base,model,local,
|
||||
keySet,keyMasked`, `ai.py` L36–58).
|
||||
- **Ruling 8 (эндпоинты этапа и границы):** файлы `Deal.Api/Endpoints/*`, группы
|
||||
`MapSettingsEndpoints` (GET/PATCH `/settings`), `MapRatesEndpoints` (GET `/rates`,
|
||||
POST `/rates/refresh`), `MapAiCheckEndpoint` (POST `/ai/check`), `MapMlEndpoints` (/ml/*),
|
||||
`MapFilterTesterEndpoints` (POST `/admin/check-message`). «Только для Settings-экрана»:
|
||||
GET/PATCH `/settings`, POST `/ai/check`, GET/POST `/rates*`, POST `/admin/check-message`,
|
||||
ML-статус/сброс/проверка. «Переиспользуются этапами 3+»: `GET/PATCH /settings` — общий
|
||||
источник настроек для pipeline/kanban/projects/discovery; `/api/ml/*` — счётчики и обучение
|
||||
(этап 3), предсказания (этап 4), кандидаты/apply оживают с telegram-данными (этап 6);
|
||||
правила `IncomingRules` — этап-1 пайплайна (этап 4). НЕ входят в этап 2 (зависимости):
|
||||
`/api/tg/*` (этап 6), `admin/tick`, `admin/fts/rebuild` (кнопки «Хранение и очистка» — этапы
|
||||
3/4), `/api/ai/suggest-keywords` и `suggest-columns` (этапы 3/6), `/api/columns/*`,
|
||||
`/api/leads/*`, `/api/boards/*`, `/api/projects/*`, `/api/pipeline/*`, `/api/discovery/*`,
|
||||
`/api/meta/constants` (фронт не вызывает — api-map п.9 L399), `ml/learn`, `ml/flush` (там же),
|
||||
события SSE, лимиты ТЗ §9 (этап 7).
|
||||
- **Ruling 9 (колонки/colState):** колонки и их правила — этап 3 (Kanban). В этапе 2 `colState`
|
||||
— обычный dict-ключ (passthrough в PATCH, дефолт `{}`), отдельные `/api/columns/*` НЕ делаются.
|
||||
- **Ruling 10 (звук/вид/напоминания):** `soundOn`/`volume` и тема — локальное состояние фронта
|
||||
(`store.js` L106–111, в PATCH не шлются) — бэкенд не нужен. Общие напоминания — ключ
|
||||
`remindersEnabled` (passthrough); отложенные напоминания и `reminder_due` — этап 5 (Projects).
|
||||
- **Ruling 11 (приёмка и фронт):** Vue `boot()` (`store.js` L571–581) требует отсутствующие до
|
||||
этапа 3 группы (`/boards`, `/leads`, `/leads/counts`, `/projects`, `/tg/status`,
|
||||
`/columns/state`) — полная работа фронта восстанавливается этапом 3; поэтому приёмка этапа 2 —
|
||||
unit-тесты + curl + psql. Строки ошибок/сообщений — фиксированные из прототипа (см. задачи).
|
||||
|
||||
## Задачи
|
||||
|
||||
Сокращения путей: `S=` `src/core/Deal.Modules.Settings/`, `I=` `src/core/Deal.Infrastructure/`,
|
||||
`A=` `src/core/Deal.Api/`, `C=` `src/core/Deal.Contracts/`, `T=` `src/core/tests/Deal.Tests.Unit/`.
|
||||
|
||||
### Task 1: Шифрование секретов (AES-GCM) — фундамент хранения ключей AI/Telegram
|
||||
|
||||
**Files:**
|
||||
- Create: `S/Application/ISecretCipher.cs` — `Encrypt(string)→string` (префикс `enc:`),
|
||||
`Decrypt(string)→string` (без префикса — вернуть как есть; сбой → `""`), `MaybeEncrypt`.
|
||||
- Create: `I/Security/AesGcmSecretCipher.cs` — AES-256-GCM, nonce 12/tag 16, формат
|
||||
`enc:` + Base64(nonce‖ct‖tag) (Ruling 2).
|
||||
- Create: `I/Security/EncryptionKeyProvider.cs` — ключ из `IConfiguration` (`DEAL_ENCRYPTION_KEY`,
|
||||
Base64 32 байта); fallback: файл `data/encryption.key` (env `DEAL_ENCRYPTION_KEY_FILE`),
|
||||
генерация при первом старте + warning; невалидный env-ключ → исключение при старте
|
||||
(семантика `crypto._get_fernet`, `crypto.py` L22–42).
|
||||
- Create: `A/Configuration/EncryptionOptions.cs` (IOptions: секция `Encryption`: `KeyFilePath`,
|
||||
дефолт `data/encryption.key`).
|
||||
- Modify: `I/ServiceCollectionExtensions.cs` — регистрация `ISecretCipher→AesGcmSecretCipher`
|
||||
(singleton, ключ из provider).
|
||||
- Test: `T/SecretCipherTests.cs` (roundtrip; префикс `enc:`; незашифрованная строка проходит
|
||||
как есть; повреждённый токен → `""`; `MaybeEncrypt("")` → `""`).
|
||||
|
||||
**Источники:** `backend/app/crypto.py` L1–70; `backend/app/config.py` L48–51.
|
||||
|
||||
**Acceptance:** build 0/0; `dotnet test` — SecretCipherTests PASS. Отчёт: `task-1-report.md`.
|
||||
|
||||
### Task 2: Модуль Settings — каталог ключей, дефолты, DTO, порт хранилища
|
||||
|
||||
**Files:**
|
||||
- Create: `S/Application/SettingKind.cs` (enum: Int/Bool/String/List/Dict/MyPrompts/AiConfigs/TgKeys/Internal).
|
||||
- Create: `S/Application/SettingsKeys.cs` — статический каталог публичных ключей
|
||||
(категория каждого ключа, 1:1 список §4.6 и PATCH-список L340): Int — `archiveAfterDays`,
|
||||
`archiveClearDays`, `trashClearDays`, `minLen`, `discJoinLimit`, `discJoinDelayMin/Max`,
|
||||
`discEvalSample`, `discEvalThreshold`; Bool — `autoArchive`, `aiEnabled`, `aiFilterEnabled`,
|
||||
`conversionOn`, `remindersEnabled`, `mlEnabled`, `blockResumes`, `budgetRequiredHire/Order`,
|
||||
`autoMonitorNew`, `discPaused`; String — `targetCurrency`, `rateSource`, `aiProvider`,
|
||||
`aiPrompt`, `aiFilterPrompt`, `cardPrompt`, `domainDescription`, `wantedType`, `hireLabel`,
|
||||
`orderLabel`; List — `stopPhrases`, `domainKeywords`, `hireMarkers`, `levelTerms`,
|
||||
`resumeMarkers`; Dict — `colState`; + special: `myPrompts`, `aiConfigs`, `tgKeys`; Internal:
|
||||
`ratesCache`, `mlDecisions`, `aiDecisions` (в PATCH/GET не участвуют, Ruling 1).
|
||||
- Create: `S/Application/SettingsDefaults.cs` — значения по умолчанию из `constants.py` L189–245
|
||||
(включая дефолтные стоп-фразы L55, `minLen=24`, hire/level/resume-маркеры L144–167,
|
||||
`aiConfigs` для каждого провайдера с первым `model`, `tgKeys={apiId:"",apiHash:""}`).
|
||||
- Create: `S/Application/DefaultPrompts.cs` — константы `DefaultAiPrompt`, `DefaultCardPrompt`,
|
||||
`DefaultAiFilterPrompt` — тексты КОПИРУЮТСЯ из `src/frontend/src/data.js` L94–141 (фронт —
|
||||
источник; в `constants.py` L63–141 те же тексты для сверки).
|
||||
- Create: `S/Application/AiProviderDefinition.cs` (record: Id, Name, Base, Local, Models,
|
||||
ApiStyle? `null`=OpenAI-совместимый, `"anthropic"`), `S/Application/AiProviders.cs`
|
||||
(статический список 7 провайдеров: deepseek/openai/openrouter/anthropic/ollama/lmstudio/custom —
|
||||
`constants.py` L170–186).
|
||||
- Create: `S/Application/MockRates.cs` (константа, `constants.py` L41–50) + `RatesFetchInterval = 6h`.
|
||||
- Create: `S/Application/ISettingsStore.cs` — порт: `Task<object?> GetAsync(string key, ct)`,
|
||||
`Task<Dictionary<string,object?>> GetAllAsync(ct)`, `Task SetAsync(string key, object? value, ct)`
|
||||
(значения JSON-сериализуемые; список/словарь/строка/число/булево).
|
||||
- Test: `T/SettingsCatalogTests.cs` (все ключи §4.6 присутствуют с корректной категорией;
|
||||
внутренние ключи не в каталоге публичных; провайдеры: 7 шт., id/base соответствуют списку;
|
||||
MockRates содержит RUB/USD/EUR/USDT).
|
||||
|
||||
**Источники:** api-map §4.6 L315–341; `constants.py`; `data.js` L6–141.
|
||||
|
||||
**Acceptance:** build 0/0; SettingsCatalogTests PASS. Отчёт: `task-2-report.md`.
|
||||
|
||||
### Task 3: SettingsService — public-снимок и частичное обновление (PATCH-семантика 1:1)
|
||||
|
||||
**Files:**
|
||||
- Create: `S/Application/Models/PublicSettingsDto.cs` — record со всеми полями §4.6
|
||||
(вложенные: `MyPromptDto{Id,Name,Description,Prompt}`, `AiConfigPublicDto{BaseUrl,Model,KeySet,
|
||||
KeyMasked}`, `TgKeysPublicDto{ApiId,ApiHashSet}`, `ProviderPublicDto{Id,Name,Base,Local,Models}`).
|
||||
- Create: `S/Application/SettingsService.cs` — `GetPublicAsync(ct)` (дефолты+сохранённые,
|
||||
маскирование, Ruling 3; для `apiHashSet` — `SecretCipher.Decrypt(apiHash) != ""`, для каждого
|
||||
провайдера — расшифровка ключа + `keySet/keyMasked`); `ApplyPatchAsync(
|
||||
Dictionary<string,JsonElement> body, ct)` с клампами и валидацией (см. ниже), ответ — полный
|
||||
public-снимок (фронт затирает локальный state ответом — api-map L147, L341).
|
||||
- Create: `T/…/FakeSettingsStore.cs` (in-memory Dictionary), `T/SettingsServiceTests.cs`.
|
||||
|
||||
**Семантика PATCH (референс `settings_routes.py` L75–192):**
|
||||
- Int: нечисловое → пропуск ключа; клампы: `archiveAfterDays` 1..30, `minLen` 10..500,
|
||||
`discJoinLimit` 1..200, `discJoinDelayMin/Max` 5..600, `discEvalSample` 3..30,
|
||||
`discEvalThreshold` 1..100; интервалы задержек: при паре — клампы+swap при min>max; при одном
|
||||
конце — кламп относительно сохранённого другого конца (L80–109).
|
||||
- Bool: JSON-булево (строки не «питон-булеватся»). String: `targetCurrency` → Upper;
|
||||
`aiProvider` вне списка провайдеров → пропуск; остальные — строка как есть.
|
||||
- List: только список → строки, срез 200. Dict: `colState` — как есть (Ruling 9).
|
||||
- `myPrompts`: ≤100; name≤80, prompt≤8000, description≤300 (trim); пустые name/prompt — дроп;
|
||||
id ≤40 или генерация `pp_` + 8 hex (Ruling дефолта, референс L143–160).
|
||||
- `aiConfigs`: только существующие провайдеры; `baseUrl`/`model` — строки; `apiKey` непустой,
|
||||
≥8 симв., без префикса `enc:` → шифруется (L161–175).
|
||||
- `tgKeys`: `apiId` — только цифры, длина 6..9 (5<len<10); `apiHash` ≥16 симв. → шифруется
|
||||
(L176–185).
|
||||
- Побочные эффекты PATCH: при `rateSource` — запуск `RatesService.RefreshAsync` (fire-and-forget);
|
||||
при `targetCurrency`/`conversionOn` — в этапе 2 ничего (нет leads; этап 3) (L186–192).
|
||||
|
||||
**Источники:** api-map §4.6 L147, L340–341; `settings_routes.py` целиком; `crypto.py`.
|
||||
|
||||
**Acceptance:** `dotnet test` — SettingsServiceTests PASS: снимок дефолтов; маскирование ключа;
|
||||
каждый кламп; swap интервалов; `myPrompts` clean+id; шифрование aiConfigs/tgKeys (в БД `enc:`);
|
||||
неизвестный ключ игнорируется. Отчёт: `task-3-report.md`.
|
||||
|
||||
### Task 4: KV-адаптер SettingsStore (EF) и DI
|
||||
|
||||
**Files:**
|
||||
- Create: `I/Persistence/Repositories/SettingsStore.cs` — реализует `ISettingsStore` на
|
||||
`TenantDbContext.Settings` (сущность `TenantSettingEntity` уже есть): чтение всех строк,
|
||||
сериализация/десериализация значений в JSON, `updated_at` — UTC-now.
|
||||
- Modify: `I/ServiceCollectionExtensions.cs` — `AddScoped<ISettingsStore, SettingsStore>()`;
|
||||
регистрация `ISecretCipher` из Task 1, `RatesService`-зависимостей из Tasks 6–8.
|
||||
- Modify: `S/SettingsModuleRegistrar.cs` (Create) — `AddSettingsModule()`: `SettingsService`,
|
||||
`RatesService`, `IncomingRules` (scoped); вызывается в `A/Program.cs` (Task 5).
|
||||
- Modify: `A/Deal.Api.csproj` — ProjectReference на `Deal.Modules.Settings`.
|
||||
|
||||
**Источники:** эталон: `I/Persistence/Repositories/AuthStore.cs`, `TenantModuleRegistrar.cs`,
|
||||
`ServiceCollectionExtensions.cs` (этап 1).
|
||||
|
||||
**Acceptance:** build 0/0; psql-проверка: GET через сервис на пустой схеме тенанта возвращает
|
||||
дефолты, `SetAsync` создаёт строку с `value_json`. Отчёт: `task-4-report.md`.
|
||||
|
||||
### Task 5: Эндпоинты GET/PATCH /api/settings + DI + curl-приёмка
|
||||
|
||||
**Files:**
|
||||
- Create: `A/Endpoints/SettingsEndpoints.cs` (`MapSettingsEndpoints`): `GET /api/settings` →
|
||||
PublicSettingsDto; `PATCH /api/settings` — тело произвольный JSON-объект →
|
||||
полный снимок после применения. Авторизация — через `SessionMiddleware`/`CurrentUser`
|
||||
(эталон `AuthEndpoints.cs`), 401 `{"detail":"Требуется авторизация"}`.
|
||||
- Modify: `A/Program.cs` — `AddSettingsModule()`, map групп эндпоинтов.
|
||||
- Модификации предыдущих задач собираются здесь же (порядок исполнения: T1→T4 затем T5).
|
||||
|
||||
**Контракт (api-map §3.4 L146–147, §4.6):** GET — все ключи §4.6 (camelCase, дефолты, маски,
|
||||
`providers` список); PATCH — те же поля-группы, что шлёт фронт (L340), ответ — полный снимок.
|
||||
Ошибок-исключений нет (мягкая семантика: невалидное поле просто не применяется).
|
||||
|
||||
**Acceptance (curl, cookie-сессия admin/admin):**
|
||||
1. `GET /api/settings` → дефолты: `aiEnabled:true, mlEnabled:true, minLen:24,
|
||||
archiveAfterDays:14, stopPhrases:[4 дефолтные], wantedType:"both", rateSource:"cbr",
|
||||
aiProvider:"deepseek", tgKeys:{apiId:"", apiHashSet:false}, colState:{}`, `providers` — 7.
|
||||
2. `PATCH {"archiveAfterDays":99,"minLen":3,"discJoinDelayMin":700,"discJoinDelayMax":5}` →
|
||||
в ответе `archiveAfterDays:30, minLen:10, discJoinDelayMin:5, discJoinDelayMax:700` (swap).
|
||||
3. `PATCH {"myPrompts":[{name:"x",prompt:"y"},{name:"",prompt:""}]}` → 1 элемент, `id` начинается `pp_`.
|
||||
4. `PATCH {"aiConfigs":{"deepseek":{"apiKey":"sk-1234567890ab"}}}` → ответ `keySet:true,
|
||||
keyMasked:"sk-1…90ab"`; psql: `value_json` содержит `enc:` (см. Task 7-контракт psql).
|
||||
5. `PATCH {"tgKeys":{"apiId":"123456","apiHash":"abcdefghijklmnop"}}` → `apiHashSet:true`.
|
||||
6. Неизвестный ключ `{"foo":1}` — без ошибки, снимок без `foo`.
|
||||
Отчёт: `task-5-report.md`.
|
||||
|
||||
### Task 6: ИИ-провайдеры и POST /api/ai/check (проверка подключения)
|
||||
|
||||
**Files:**
|
||||
- Create: `S/Application/IAiConnectionChecker.cs` — `Task<AiCheckResultDto> CheckAsync(
|
||||
AiCheckRequest request, ct)`, `S/Application/Models/AiCheckResultDto.cs` (Ok, Message, Provider,
|
||||
Name, Base, Model, Local, KeySet, KeyMasked), `AiCheckRequest` (ProviderId, BaseUrl, Model,
|
||||
ApiKey, IsLocal, ApiStyle).
|
||||
- Create: `I/Integrations/AiConnectionChecker.cs` — HTTP-реализация (Ruling 7) через
|
||||
`IHttpClientFactory` (таймаут 12 с), переиспользует формат сообщений прототипа.
|
||||
- Create: `A/Endpoints/AiCheckEndpoint.cs` (`MapAiCheckEndpoint`) — читает активную конфигурацию
|
||||
провайдера из `ISettingsStore` (расшифровка ключа через `ISecretCipher`), вызывает checker,
|
||||
отдаёт `{ok,message,provider,name,base,model,local,keySet,keyMasked}` (api-map §4.10 L365).
|
||||
- Test: `T/AiConnectionCheckerTests.cs` (fake `HttpMessageHandler`): без ключа; local; 200;
|
||||
401; 403; HTTP 500; сетевая ошибка.
|
||||
|
||||
**Источники:** `settings_routes.py` L195–219; `ai.py` L36–58 (provider_status + mask_key).
|
||||
|
||||
**Acceptance:** build 0/0; тесты PASS. curl: без ключа → `{"ok":false,"message":"Не задан
|
||||
API-ключ",...}`; провайдер `ollama` → ok:true «Локальный сервер…»; `deepseek` с неверным ключом
|
||||
и недоступным хостом → `"Ошибка соединения: …"` (сеть недоступна — допустимо). Отчёт:
|
||||
`task-6-report.md`.
|
||||
|
||||
### Task 7: Промпты и «Мои промпты» — интеграционная проверка границы с фронтом
|
||||
|
||||
Бэкенд-логика уже в Tasks 2–3 (`DefaultPrompts`, валидация `myPrompts`). Задача — контроль
|
||||
1:1 границы и приёмочные проверки (библиотека по сферам — фронтовая, `data.js` PROMPT_LIBRARY
|
||||
L180–200; `PromptLibraryModal.vue` не ходит в API; наружу идут только промпты-строки и
|
||||
`myPrompts`).
|
||||
|
||||
**Files:**
|
||||
- Test: `T/PromptDefaultsTests.cs` — дефолтные тексты начинаются/содержат маркеры из
|
||||
`data.js` (например `aiPrompt` содержит «Ты — классификатор входящих сообщений» и
|
||||
плейсхолдеры `{domain}`/`{keywords}`; `cardPrompt` — «О заявке»; `aiFilterPrompt` — «страж
|
||||
входящих»); `fill_prompt`-подстановка (аналог `ai.fill_prompt` L63–77): пустой domain →
|
||||
фраза-фолбэк, keywords склейка, ≤60 ключей.
|
||||
- Create: `S/Application/PromptFiller.cs` — подстановка `{domain}`/`{keywords}` (чистая функция,
|
||||
используется этапом 6 для ИИ-вызовов).
|
||||
|
||||
**Acceptance (curl):** 1) PATCH `aiPrompt` с плейсхолдерами → GET возвращает тот же текст;
|
||||
2) PATCH `myPrompts` 3 записи → GET отдаёт их (camelCase `id/name/description/prompt`);
|
||||
3) «Применить из библиотеки» фронта = локальная операция — API не вызывается. `dotnet test`
|
||||
PromptDefaultsTests PASS. Отчёт: `task-7-report.md`.
|
||||
|
||||
### Task 8: Курсы валют — сервис, кэш, эндпоинты /api/rates*
|
||||
|
||||
**Files:**
|
||||
- Create: `S/Application/IRatesSource.cs` — порт: `Task<Dictionary<string,double>?> FetchAsync(ct)`
|
||||
(курсы к RUB). `S/Application/Models/RatesDto.cs` — record `{Base, Rates, Source, UpdatedAtMs?}`.
|
||||
- Create: `S/Application/RatesService.cs` — `GetAsync(ct)` (кэш `ratesCache`; нет кэша → дефолт
|
||||
MockRates/source "mock"/updatedAt null); `RefreshAsync(ct)` (source из настройки: mock →
|
||||
сохранить MockRates; cbr → `IRatesSource`; неуспех → `false`, кэш не трогаем); `ShouldFetch(ct)`
|
||||
(нет кэша / смена источника / ≥6 ч, `rates.py` L77–83); `ConvertAmount(amount, fromCur, toCur)`
|
||||
— USDT→USD (L86–103). Ленивое обновление на GET при `ShouldFetch` — фоновый запуск
|
||||
`RefreshAsync`, ответ — текущий кэш.
|
||||
- Create: `I/Integrations/CbrRateSource.cs` — HTTP GET `https://www.cbr-xml-daily.ru/daily_json.js`
|
||||
(JSON), `Valute[code].Value/Nominal`, `RUB:1`; сбой → null (лог) (`rates.py` L43–59).
|
||||
- Create: `A/Endpoints/RatesEndpoints.cs` (`MapRatesEndpoints`): `GET /api/rates` → RatesDto;
|
||||
`POST /api/rates/refresh` → `{ok, rates: RatesDto}` (ok=false при сбое cbr; при mock — true).
|
||||
- Modify: `A/Program.cs` — map; DI: `IRatesSource→CbrRateSource` (scoped), `AddHttpClient`.
|
||||
- Test: `T/RatesServiceTests.cs` (fake store+source): mock-режим; cbr успех/сбой; ShouldFetch
|
||||
(интервал 6 ч, смена источника); ConvertAmount USDT=USD, отсутствующая валюта → null.
|
||||
|
||||
**Источники:** `services/rates.py` целиком; api-map §3.4 L149–150; `settings_routes.py` L224–232.
|
||||
|
||||
**Acceptance:** build 0/0; тесты PASS. curl: `PATCH {"rateSource":"mock"}` затем
|
||||
`POST /api/rates/refresh` → `{ok:true, rates:{base:"RUB", rates:{RUB:1,USD:92.5,…},
|
||||
source:"mock", updatedAt:<ms>}}`; `GET /api/rates` — тот же кэш. Отчёт: `task-8-report.md`.
|
||||
|
||||
### Task 9: ML-панель — порт IMlClient, детерминированная заглушка, эндпоинты /api/ml
|
||||
|
||||
**Files:**
|
||||
- Create: `C/Integrations/IMlClient.cs` + `C/Integrations/Models/*.cs` — record-DTO:
|
||||
`MlServiceStatusDto {Ready, Classes(Dictionary<string,double>), Learned, Eval{MlEvalDto}}`,
|
||||
`MlEvalDto {Count, Correct, Accuracy}`, `MlPredictResultDto {Take, Label?, Scores, Hits,
|
||||
Ready, Margin?, Terms[], Type?}`, `MlStatusResponseDto {Enabled, Service, Reachable, Stats{
|
||||
MlStatsDto}}`, `MlStatsDto {Ml, Ai, Learning, Ready, Classes, Learned, Reachable, Outbox}`
|
||||
(поля/типы 1:1 `ml_routes.py` L70–75 + `ml_client.snapshot()` L138–150).
|
||||
- Create: `I/Integrations/LocalMlClient.cs` — заглушка Ruling 5 (детерминированная; обучение
|
||||
недоступно до этапа 3 — модель всегда «не готова»; счётчики `mlDecisions/aiDecisions` —
|
||||
из KV settings, Ruling 1).
|
||||
- Create: `A/Endpoints/MlEndpoints.cs` (`MapMlEndpoints`):
|
||||
- `GET /api/ml/status` → `MlStatusResponseDto` (`enabled` = `mlEnabled !== false`);
|
||||
- `POST /api/ml/reset` → `{ok:true}` (мягкая ошибка `{ok:false,error}` — зарезервирована);
|
||||
- `POST /api/ml/predict` `{text}`: trim <2 симв. → 400 «Введите текст»; ответ
|
||||
`{text:<первые 200>, take, label, scores, hits, ready, margin, terms, type}`;
|
||||
- `POST /api/ml/candidates` `{dialogId, limit=10 (clamp 1..60)}` → `{items: []}` (данных
|
||||
telegram нет — этап 6; контракт §3.7 L196);
|
||||
- `POST /api/ml/apply` `{dialogId, msgId, action}` → 404 «Исходное сообщение не найдено»
|
||||
(нет сообщений до этапов 3/6; ветка `skip` — этап 6; контракт §3.7 L197).
|
||||
- НЕ реализуем: `ml/learn`, `ml/flush` (фронт не вызывает, api-map п.9).
|
||||
- Modify: `A/Program.cs` — DI `IMlClient→LocalMlClient` (scoped), map.
|
||||
- Test: `T/LocalMlClientTests.cs` (status-форма; predict неготовой модели — все поля; reset → ok).
|
||||
|
||||
**Источники:** api-map §3.7, §4.10 L363; `ml_routes.py` L66–91, L112–171; `ml_client.py` L127–150;
|
||||
`mlservice/model.py` (predict L184–293, status L325–345 — эталон полей для этапа 6).
|
||||
|
||||
**Acceptance:** build 0/0; тесты PASS. curl: login → `GET /api/ml/status` (все поля, `reachable:
|
||||
true`, `ready:false`, `stats.outbox:0`); `POST /api/ml/predict {"text":"x"}` → 400;
|
||||
`POST /api/ml/predict {"text":"Python backend на fastapi, бот в телеграм"}` → `take:false,
|
||||
label:null, scores:{}, ready:false`; `POST /api/ml/reset` → `{ok:true}`; `POST /api/ml/candidates`
|
||||
→ `{"items":[]}`. Отчёт: `task-9-report.md`.
|
||||
|
||||
### Task 10: Тестер фильтров — этап-1 правила и POST /api/admin/check-message
|
||||
|
||||
**Files:**
|
||||
- Create: `S/Application/IncomingRules.cs` — чистая реализация `stage1_plain` (`pipeline.py`
|
||||
L94–124) поверх `ISettingsStore`: минимальная длина (`minLen`), стоп-фразы (casefold, ответ —
|
||||
конкретная фраза), блокировка резюме (`blockResumes` + `resumeMarkers` с контекстным guard
|
||||
«вакансия… присылайте резюме» — не режем, `pipeline._resume_reason` L644–654), тип заявки
|
||||
(`wantedType` + маркеры найма `hireMarkers`); результат
|
||||
`{pass, reason, stage:1, kind:length|stop|resume|type, kw}`.
|
||||
- Create: `A/Endpoints/FilterTesterEndpoints.cs` (`MapFilterTesterEndpoints`):
|
||||
`POST /api/admin/check-message` `{text}` → `{stage1:{pass,reason}, stage2, passed}`
|
||||
(1:1 `dashboard_routes.py` L267–284): если этап-1 не прошёл → `stage2:{pass:false,reason:null,
|
||||
skipped:true}, passed:false`; иначе `stage2:{pass:true,reason:null,skipped:true}` — ИИ-фильтр
|
||||
на этапе 2 всегда skipped (Ruling 4/8; реальный ИИ-фильтр — этап 6).
|
||||
- Test: `T/IncomingRulesTests.cs`: короткий текст; стоп-фраза из настроек; резюме (маркер);
|
||||
guard «…вакансия… присылайте резюме» → pass; `wantedType:"freelance"` с вакансионным маркером;
|
||||
`wantedType:"vacancy"` с разовым заказом.
|
||||
|
||||
**Источники:** api-map §3.2 L109, §4.10 L364; `dashboard_routes.py` L267–284; `pipeline.py`
|
||||
L94–124; фронт: `SettingsView.vue` L142–155 (тестер), `store.js` L1723–1725.
|
||||
|
||||
**Acceptance:** build 0/0; тесты PASS. curl: с дефолтами текст «Заработок на крипте…» (длина
|
||||
≥24, без стоп-фраз) → `stage1.pass:true, stage2.skipped:true, passed:true`; текст «Ищу работу
|
||||
python» → `stage1.pass:false, kind:"resume"` (если ≥minLen); «взаимный пиар» внутри → `kind:
|
||||
"stop"`. Отчёт: `task-10-report.md`.
|
||||
|
||||
### Task 11: Финал этапа — интеграция и сквозная приёмка
|
||||
|
||||
- `scripts/build.sh`/`scripts/test.sh` — успешны; `dotnet build Deal.sln` 0/0; все unit-тесты PASS.
|
||||
- Сквозной curl-сценарий Settings-экрана: login admin/admin → GET /settings →
|
||||
PATCH-группы из Tasks 5/7 (обработка, ИИ-промпты, myPrompts, aiConfigs, tgKeys, валюта,
|
||||
хранение/уведомления: `autoArchive/archiveAfterDays/remindersEnabled`, colState) →
|
||||
POST /ai/check → GET /rates + POST /rates/refresh (mock) → GET /api/ml/status + predict +
|
||||
reset → POST /api/admin/check-message (pass и отсев).
|
||||
- psql-проверка схемы дефолтного тенанта (`SET search_path TO tenant_00000000000000000000000000000001;`):
|
||||
строки settings созданы, `value_json` для aiConfigs/tgKeys содержит `enc:` и не содержит
|
||||
открытого ключа; внутренние ключи (`ratesCache`, `mlDecisions`) не появляются в GET /settings.
|
||||
- Известные ограничения этапа (зафиксировать в отчёте): Telegram-вкладка, кнопки «Проверить
|
||||
правила сейчас»/«Пересобрать индекс» (admin/tick, admin/fts), «Предложить ключи»
|
||||
(ai/suggest-keywords) и весь канбан-фронт не работают до этапов 3–6 (Ruling 8/11).
|
||||
- Обновить `docs/technical/Техническая-документация-Дейл.md` (раздел настроек: env
|
||||
`DEAL_ENCRYPTION_KEY`, поведение GET/PATCH /settings, креды).
|
||||
- Отчёт `task-11-report.md` + финальная строка в `progress.md`.
|
||||
|
||||
## Self-Review
|
||||
|
||||
1. **Spec coverage:** ТЗ §8 (настройки тенанта) — Tasks 1–10; §5 (этап-1 фильтры/тип/резюме —
|
||||
только настройки+тестер) — Task 10, (ML/ИИ-слои пайплайна — этапы 4/6, вне); §7 (обработка) —
|
||||
вне (этап 4); §9 (лимиты) — вне; api-map §3.4 — Tasks 5/7/8; §3.7 — Task 9; admin/check-message
|
||||
— Task 10; §4.6/4.7 — Tasks 2/3/5/7; tgKeys-часть §4.6 — Task 3/5; шифрование §8 архитектуры —
|
||||
Task 1.
|
||||
2. **Placeholder scan:** конкретные адаптеры и контракты; «заглушки» только там, где разрешено
|
||||
решением владельца (п.5): `LocalMlClient` (Task 9), ИИ-фильтр в тестере = skipped (Task 10);
|
||||
референсы на строки файлов точные. FIXME/TODO нет.
|
||||
3. **Type consistency:** один модуль Settings владеет каталогом ключей/дефолтами — Kanban/Pipeline
|
||||
(этапы 3/4) читают те же ключи через `ISettingsStore`; `IMlClient`-контракт (Contracts)
|
||||
един для панели (этап 2), счётчиков (этап 3) и предсказаний (этап 4); сущность
|
||||
`TenantSettingEntity` не меняется; схемы/миграции не добавляются.
|
||||
4. **Вне scope этапа 2:** канбан-колонки/карточки/архив-корзина и их эндпоинты (этап 3),
|
||||
pipeline/очередь/отсев/дедуп (этап 4), projects/напоминания-отложенные/файлы (этап 5),
|
||||
реальные ml/ai/telegram-сервисы и /api/tg/* (этап 6), discovery, оператор/инвайты/лимиты/
|
||||
аудит (этап 7); colState-эндпоинты; библиотека промптов (фронтовая); звук/вид (фронт).
|
||||
# Дейл (Deal) — Этап 2: Настройки тенанта (Settings) Implementation Plan
|
||||
|
||||
> Исторический документ этапа 2. Актуальное состояние — `docs/superpowers/STATUS.md` и `docs/technical/Техническая-документация-Дейл.md`.
|
||||
|
||||
**Goal:** Реализовать в модульном монолите `src/core` модуль Settings с 1:1-контрактом `/api`,
|
||||
который потребляет экран «Настройки» Vue-фронта (`src/frontend/src/views/SettingsView.vue`,
|
||||
`components/MLPanel.vue`, `PromptLibraryModal.vue`): чтение/сохранение дерева настроек тенанта
|
||||
(таблица `settings` уже есть), шифрование секретов (ключи AI/Telegram), проверка подключения
|
||||
AI-провайдера, курсы валют, ML-панель на детерминированной локальной заглушке, тестер фильтров
|
||||
входящих. К концу этапа Settings-экран обслуживается бэкендом полностью (кроме зон, помеченных
|
||||
зависимостями этапов 3–6); приёмка — curl/psql/unit-тесты (Vue-фронт полностью оживает только
|
||||
с этапом 3: его `boot()` требует `/api/boards`, `/api/leads`, `/api/projects`, `/api/tg/status` —
|
||||
см. Ruling 11).
|
||||
|
||||
**Architecture:** новый модуль `Deal.Modules.Settings` (чистый, без EF): константы/дефолты,
|
||||
типизированный каталог ключей, порты `ISettingsStore`/`ISecretCipher`/`IRatesSource`/
|
||||
`IAiConnectionChecker`, сервисы `SettingsService` (public-снимок + частичный PATCH), `RatesService`,
|
||||
`IncomingRules` (этап-1 правила тестера). Адаптеры — в `Deal.Infrastructure`: KV `SettingsStore`
|
||||
(таблица `settings`, JSON в `value_json`), `AesGcmSecretCipher`, `CbrRateSource`, HTTP-проверка AI.
|
||||
Интеграционный порт `IMlClient` + record-DTO — в `Deal.Contracts/Integrations`, заглушка
|
||||
`LocalMlClient` — в `Deal.Infrastructure/Integrations`. HTTP-эндпоинты — в `Deal.Api/Endpoints/`
|
||||
(`MapSettingsEndpoints`, `MapMlEndpoints`, `MapFilterTesterEndpoints`). Внешние сервисы
|
||||
(реальные ml/ai/telegram) на этапе 6 заменят заглушки gRPC-адаптерами без правки эндпоинтов.
|
||||
|
||||
**Spec:** `docs/api/api-map.md` §3.4 (L142–152), §3.7 (L187–199), §4.6 (L315–341), §4.7 (L343–346),
|
||||
§4.10 (L363–365), правила L7–24, п.9 «экономия» (L399); `docs/spec/ТЗ-дейл-новая-архитектура.md`
|
||||
§8 (L165–179), §5 (L89–121, фильтры), §7 (L150–161 — только пересечения), §9 (лимиты — НЕ в этап);
|
||||
`docs/architecture/2026-09-05-deal-architecture-design.md` §5 (границы модулей), §8 (секреты L207);
|
||||
референс-семантика: `backend/app/routers/settings_routes.py`, `backend/app/services/rates.py`,
|
||||
`backend/app/services/ai.py` (L36–77, L188–198), `backend/app/routers/ml_routes.py`,
|
||||
`backend/app/services/ml_client.py`, `backend/app/routers/dashboard_routes.py` (admin/check-message
|
||||
L267–284), `backend/app/services/pipeline.py` (stage1_plain L94–124), `backend/app/constants.py`
|
||||
(L30–50, L54–245), `backend/app/crypto.py`, `backend/app/config.py` (L48–51);
|
||||
фронт: `src/frontend/src/store.js` (boot L565–628, applySettings L343–397, applyMlStatus L487–502,
|
||||
schedulePersist L1737–1766, refreshRates L1844–1848), `src/frontend/src/data.js` (L6–141 дефолты
|
||||
промптов; `AI_PROVIDERS` L17–80; `PROMPT_LIBRARY` L180–200 — библиотека по сферам живёт ТОЛЬКО
|
||||
во фронте, бэкенд её не отдаёт), `views/SettingsView.vue` (вкладки L39–49), `components/MLPanel.vue`,
|
||||
`components/PromptLibraryModal.vue`.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Проект **НЕ git**; фиксация — отчёты задач `task-N-report.md` и `progress.md`. Рабочая папка плана: `.superpowers/sdd/deal-stage2-settings/`.
|
||||
- .NET 10 SDK, решение собирается с 0 warnings / 0 errors (`TreatWarningsAsErrors`).
|
||||
- Код-стайл этапа 1: 1 тип = 1 файл; XML-doc на public-контракты; комментарии на русском; явные модификаторы; настройки через `IOptions<T>`; без регионов.
|
||||
- namespace `Deal.*`. Секретов в коде нет; ключи шифрования — env/файл (Ruling 2). `tenantId` — только из сессии.
|
||||
- Таблица `settings` уже в `TenantDbContext` (миграция `InitialTenant`) — новые EF-таблицы в этапе 2 НЕ создаются.
|
||||
- LeadRadar-контейнеры, `backend/`, `mlservice/`, `src/frontend/` не трогаем. Dev-Postgres `deal-postgres` (:5433).
|
||||
- Ответы: camelCase JSON; ошибки — HTTP + `{"detail"}`; «мягкие» ошибки (ml/reset) — HTTP 200 с полем `error`.
|
||||
- Дефолтные значения настроек/промптов — из констант прототипа `constants.py` и `data.js` (фронт — высший авторитет форм; тексты промптов копируются из `data.js` L94–141).
|
||||
|
||||
## Зафиксированные решения (Rulings этапа)
|
||||
|
||||
- **Ruling 1 (модель настроек):** типизированные ключи в существующей таблице `settings`
|
||||
(`key` text PK, `value_json` — JSON-сериализованное значение любого типа, `updated_at`).
|
||||
Модуль хранит только переопределения; дефолты — в коде (`SettingsDefaults`), при чтении
|
||||
снимок = дефолты, перекрытые сохранёнными значениями. Каталог публичных ключей —
|
||||
статический словарь «ключ → категория» (Int/Bool/String/List/Dict/MyPrompts/AiConfigs/TgKeys).
|
||||
Внутренние (непубличные) ключи — `ratesCache`, `mlDecisions`, `aiDecisions` — хранятся в той же
|
||||
таблице через `ISettingsStore`, но в GET/PATCH `/settings` не участвуют. Неизвестные ключи в
|
||||
PATCH игнорируются (семантика `settings_routes.py` L110–185).
|
||||
- **Ruling 2 (шифрование секретов):** AES-256-GCM (`System.Security.Cryptography.AesGcm`),
|
||||
nonce 12 байт, tag 16 байт. Ключ — env `DEAL_ENCRYPTION_KEY` (32 байта, urlsafe-Base64);
|
||||
при отсутствии в dev — файл `<ContentRoot>/data/encryption.key` (генерируется при первом
|
||||
старте, лог-warning; путь переопределяется env `DEAL_ENCRYPTION_KEY_FILE`). Формат значения в
|
||||
БД: `enc:` + Base64(nonce‖ct‖tag). Расшифровка повреждённого/чужого значения → пустая строка
|
||||
+ warning (совместимость `crypto.decrypt_text`, `crypto.py` L52–61). Порт `ISecretCipher` —
|
||||
в модуле Settings, адаптер `AesGcmSecretCipher` — в Infrastructure.
|
||||
- **Ruling 3 (маски и публичная форма):** маска `mask(v)`: пусто → `""`, `len≤8` → как есть,
|
||||
иначе `v[:4]+"…"+v[-4:]` (`settings_routes.py` L28–32). `aiConfigs` наружу —
|
||||
`{id: {baseUrl, model, keySet, keyMasked}}`; `tgKeys` — `{apiId: <маска>, apiHashSet: bool}`.
|
||||
Список `providers` — статический из модуля (`id,name,base,local,models`; зеркало
|
||||
`constants.AI_PROVIDERS` L170–186; `api_style` — внутреннее поле, наружу не отдаётся).
|
||||
- **Ruling 4 (границы интеграционных портов):** порты будущих внешних сервисов (ML/AI/telegram)
|
||||
объявляются в `Deal.Contracts/Integrations` (интерфейс + record-DTO) — их потребляют несколько
|
||||
модулей и Api. Заглушки этапа — детерминированные адаптеры в `Deal.Infrastructure/Integrations`;
|
||||
на этапе 6 заменяются gRPC-клиентами с тем же контрактом. `IAiFacade` на этапе 2 не заводится:
|
||||
классификация/фильтр ИИ — этап 6, проверка соединения — модульный порт `IAiConnectionChecker`.
|
||||
- **Ruling 5 (ML-заглушка):** `IMlClient` (Contracts): `StatusAsync/PredictAsync/ResetAsync`
|
||||
(+ `PushAsync` добавится этапом 3). `LocalMlClient` — детерминированная: `reachable=true`,
|
||||
`ready=false`, `classes={}`, `learned=0`, `eval={count:0,correct:0,accuracy:0}` (обучение на
|
||||
действиях появится с Kanban-этапом 3); `PredictAsync` неготовой модели →
|
||||
`{take:false,label:null,scores:{},hits:0,ready:false,margin:null,terms:[],type:null}`;
|
||||
`ResetAsync` → `{ok:true}`. Таблиц `ml_outbox`/`learning_log` в этапе 2 нет (владельцы —
|
||||
этапы 3/4); счётчики `mlDecisions`/`aiDecisions` — KV-настройки.
|
||||
- **Ruling 6 (курсы валют):** кэш — tenant-настройка `ratesCache` `{rates, source, updatedAtMs}`.
|
||||
Источник по `rateSource` (`cbr`|`mock`); интервал обновления 6 часов (≤4 запроса/сутки,
|
||||
`rates.py` L20); `USDT=USD` (`rates.py` L86–91). Mock-курсы — константа `MockRates`
|
||||
(`constants.py` L41–50). Обновление: лениво на GET при протухании/смене источника, синхронно
|
||||
на `POST /rates/refresh`, фоново-запуск на PATCH `rateSource` (`settings_routes.py` L186–192).
|
||||
Массовый пересчёт карточек (`recompute_conversions`) — этап 3 (таблицы leads нет); в этапе 2 —
|
||||
только чистый `ConvertAmount`.
|
||||
- **Ruling 7 (проверка AI):** реальный HTTP, без LLM-вызовов, 1:1 `settings_routes.py` L195–219:
|
||||
нет ключа → `{ok:false, message:"Не задан API-ключ"}`; локальный провайдер → `{ok:true,
|
||||
message:"Локальный сервер «<name>» (ping в проде)"}`; облачный → `GET {base}/models`
|
||||
(Anthropic: `{base}/v1/models`, заголовок `x-api-key`); HTTP<400 → ok, 401/403 → «Ключ не
|
||||
принят (HTTP n)…», иначе «HTTP n — проверьте Base URL и модель»; сетевой сбой → «Ошибка
|
||||
соединения: …». Ответ — `{ok, message}` + статус провайдера (`provider,name,base,model,local,
|
||||
keySet,keyMasked`, `ai.py` L36–58).
|
||||
- **Ruling 8 (эндпоинты этапа и границы):** файлы `Deal.Api/Endpoints/*`, группы
|
||||
`MapSettingsEndpoints` (GET/PATCH `/settings`), `MapRatesEndpoints` (GET `/rates`,
|
||||
POST `/rates/refresh`), `MapAiCheckEndpoint` (POST `/ai/check`), `MapMlEndpoints` (/ml/*),
|
||||
`MapFilterTesterEndpoints` (POST `/admin/check-message`). «Только для Settings-экрана»:
|
||||
GET/PATCH `/settings`, POST `/ai/check`, GET/POST `/rates*`, POST `/admin/check-message`,
|
||||
ML-статус/сброс/проверка. «Переиспользуются этапами 3+»: `GET/PATCH /settings` — общий
|
||||
источник настроек для pipeline/kanban/projects/discovery; `/api/ml/*` — счётчики и обучение
|
||||
(этап 3), предсказания (этап 4), кандидаты/apply оживают с telegram-данными (этап 6);
|
||||
правила `IncomingRules` — этап-1 пайплайна (этап 4). НЕ входят в этап 2 (зависимости):
|
||||
`/api/tg/*` (этап 6), `admin/tick`, `admin/fts/rebuild` (кнопки «Хранение и очистка» — этапы
|
||||
3/4), `/api/ai/suggest-keywords` и `suggest-columns` (этапы 3/6), `/api/columns/*`,
|
||||
`/api/leads/*`, `/api/boards/*`, `/api/projects/*`, `/api/pipeline/*`, `/api/discovery/*`,
|
||||
`/api/meta/constants` (фронт не вызывает — api-map п.9 L399), `ml/learn`, `ml/flush` (там же),
|
||||
события SSE, лимиты ТЗ §9 (этап 7).
|
||||
- **Ruling 9 (колонки/colState):** колонки и их правила — этап 3 (Kanban). В этапе 2 `colState`
|
||||
— обычный dict-ключ (passthrough в PATCH, дефолт `{}`), отдельные `/api/columns/*` НЕ делаются.
|
||||
- **Ruling 10 (звук/вид/напоминания):** `soundOn`/`volume` и тема — локальное состояние фронта
|
||||
(`store.js` L106–111, в PATCH не шлются) — бэкенд не нужен. Общие напоминания — ключ
|
||||
`remindersEnabled` (passthrough); отложенные напоминания и `reminder_due` — этап 5 (Projects).
|
||||
- **Ruling 11 (приёмка и фронт):** Vue `boot()` (`store.js` L571–581) требует отсутствующие до
|
||||
этапа 3 группы (`/boards`, `/leads`, `/leads/counts`, `/projects`, `/tg/status`,
|
||||
`/columns/state`) — полная работа фронта восстанавливается этапом 3; поэтому приёмка этапа 2 —
|
||||
unit-тесты + curl + psql. Строки ошибок/сообщений — фиксированные из прототипа (см. задачи).
|
||||
|
||||
## Задачи
|
||||
|
||||
Сокращения путей: `S=` `src/core/Deal.Modules.Settings/`, `I=` `src/core/Deal.Infrastructure/`,
|
||||
`A=` `src/core/Deal.Api/`, `C=` `src/core/Deal.Contracts/`, `T=` `src/core/tests/Deal.Tests.Unit/`.
|
||||
|
||||
### Task 1: Шифрование секретов (AES-GCM) — фундамент хранения ключей AI/Telegram
|
||||
|
||||
**Files:**
|
||||
- Create: `S/Application/ISecretCipher.cs` — `Encrypt(string)→string` (префикс `enc:`),
|
||||
`Decrypt(string)→string` (без префикса — вернуть как есть; сбой → `""`), `MaybeEncrypt`.
|
||||
- Create: `I/Security/AesGcmSecretCipher.cs` — AES-256-GCM, nonce 12/tag 16, формат
|
||||
`enc:` + Base64(nonce‖ct‖tag) (Ruling 2).
|
||||
- Create: `I/Security/EncryptionKeyProvider.cs` — ключ из `IConfiguration` (`DEAL_ENCRYPTION_KEY`,
|
||||
Base64 32 байта); fallback: файл `data/encryption.key` (env `DEAL_ENCRYPTION_KEY_FILE`),
|
||||
генерация при первом старте + warning; невалидный env-ключ → исключение при старте
|
||||
(семантика `crypto._get_fernet`, `crypto.py` L22–42).
|
||||
- Create: `A/Configuration/EncryptionOptions.cs` (IOptions: секция `Encryption`: `KeyFilePath`,
|
||||
дефолт `data/encryption.key`).
|
||||
- Modify: `I/ServiceCollectionExtensions.cs` — регистрация `ISecretCipher→AesGcmSecretCipher`
|
||||
(singleton, ключ из provider).
|
||||
- Test: `T/SecretCipherTests.cs` (roundtrip; префикс `enc:`; незашифрованная строка проходит
|
||||
как есть; повреждённый токен → `""`; `MaybeEncrypt("")` → `""`).
|
||||
|
||||
**Источники:** `backend/app/crypto.py` L1–70; `backend/app/config.py` L48–51.
|
||||
|
||||
**Acceptance:** build 0/0; `dotnet test` — SecretCipherTests PASS. Отчёт: `task-1-report.md`.
|
||||
|
||||
### Task 2: Модуль Settings — каталог ключей, дефолты, DTO, порт хранилища
|
||||
|
||||
**Files:**
|
||||
- Create: `S/Application/SettingKind.cs` (enum: Int/Bool/String/List/Dict/MyPrompts/AiConfigs/TgKeys/Internal).
|
||||
- Create: `S/Application/SettingsKeys.cs` — статический каталог публичных ключей
|
||||
(категория каждого ключа, 1:1 список §4.6 и PATCH-список L340): Int — `archiveAfterDays`,
|
||||
`archiveClearDays`, `trashClearDays`, `minLen`, `discJoinLimit`, `discJoinDelayMin/Max`,
|
||||
`discEvalSample`, `discEvalThreshold`; Bool — `autoArchive`, `aiEnabled`, `aiFilterEnabled`,
|
||||
`conversionOn`, `remindersEnabled`, `mlEnabled`, `blockResumes`, `budgetRequiredHire/Order`,
|
||||
`autoMonitorNew`, `discPaused`; String — `targetCurrency`, `rateSource`, `aiProvider`,
|
||||
`aiPrompt`, `aiFilterPrompt`, `cardPrompt`, `domainDescription`, `wantedType`, `hireLabel`,
|
||||
`orderLabel`; List — `stopPhrases`, `domainKeywords`, `hireMarkers`, `levelTerms`,
|
||||
`resumeMarkers`; Dict — `colState`; + special: `myPrompts`, `aiConfigs`, `tgKeys`; Internal:
|
||||
`ratesCache`, `mlDecisions`, `aiDecisions` (в PATCH/GET не участвуют, Ruling 1).
|
||||
- Create: `S/Application/SettingsDefaults.cs` — значения по умолчанию из `constants.py` L189–245
|
||||
(включая дефолтные стоп-фразы L55, `minLen=24`, hire/level/resume-маркеры L144–167,
|
||||
`aiConfigs` для каждого провайдера с первым `model`, `tgKeys={apiId:"",apiHash:""}`).
|
||||
- Create: `S/Application/DefaultPrompts.cs` — константы `DefaultAiPrompt`, `DefaultCardPrompt`,
|
||||
`DefaultAiFilterPrompt` — тексты КОПИРУЮТСЯ из `src/frontend/src/data.js` L94–141 (фронт —
|
||||
источник; в `constants.py` L63–141 те же тексты для сверки).
|
||||
- Create: `S/Application/AiProviderDefinition.cs` (record: Id, Name, Base, Local, Models,
|
||||
ApiStyle? `null`=OpenAI-совместимый, `"anthropic"`), `S/Application/AiProviders.cs`
|
||||
(статический список 7 провайдеров: deepseek/openai/openrouter/anthropic/ollama/lmstudio/custom —
|
||||
`constants.py` L170–186).
|
||||
- Create: `S/Application/MockRates.cs` (константа, `constants.py` L41–50) + `RatesFetchInterval = 6h`.
|
||||
- Create: `S/Application/ISettingsStore.cs` — порт: `Task<object?> GetAsync(string key, ct)`,
|
||||
`Task<Dictionary<string,object?>> GetAllAsync(ct)`, `Task SetAsync(string key, object? value, ct)`
|
||||
(значения JSON-сериализуемые; список/словарь/строка/число/булево).
|
||||
- Test: `T/SettingsCatalogTests.cs` (все ключи §4.6 присутствуют с корректной категорией;
|
||||
внутренние ключи не в каталоге публичных; провайдеры: 7 шт., id/base соответствуют списку;
|
||||
MockRates содержит RUB/USD/EUR/USDT).
|
||||
|
||||
**Источники:** api-map §4.6 L315–341; `constants.py`; `data.js` L6–141.
|
||||
|
||||
**Acceptance:** build 0/0; SettingsCatalogTests PASS. Отчёт: `task-2-report.md`.
|
||||
|
||||
### Task 3: SettingsService — public-снимок и частичное обновление (PATCH-семантика 1:1)
|
||||
|
||||
**Files:**
|
||||
- Create: `S/Application/Models/PublicSettingsDto.cs` — record со всеми полями §4.6
|
||||
(вложенные: `MyPromptDto{Id,Name,Description,Prompt}`, `AiConfigPublicDto{BaseUrl,Model,KeySet,
|
||||
KeyMasked}`, `TgKeysPublicDto{ApiId,ApiHashSet}`, `ProviderPublicDto{Id,Name,Base,Local,Models}`).
|
||||
- Create: `S/Application/SettingsService.cs` — `GetPublicAsync(ct)` (дефолты+сохранённые,
|
||||
маскирование, Ruling 3; для `apiHashSet` — `SecretCipher.Decrypt(apiHash) != ""`, для каждого
|
||||
провайдера — расшифровка ключа + `keySet/keyMasked`); `ApplyPatchAsync(
|
||||
Dictionary<string,JsonElement> body, ct)` с клампами и валидацией (см. ниже), ответ — полный
|
||||
public-снимок (фронт затирает локальный state ответом — api-map L147, L341).
|
||||
- Create: `T/…/FakeSettingsStore.cs` (in-memory Dictionary), `T/SettingsServiceTests.cs`.
|
||||
|
||||
**Семантика PATCH (референс `settings_routes.py` L75–192):**
|
||||
- Int: нечисловое → пропуск ключа; клампы: `archiveAfterDays` 1..30, `minLen` 10..500,
|
||||
`discJoinLimit` 1..200, `discJoinDelayMin/Max` 5..600, `discEvalSample` 3..30,
|
||||
`discEvalThreshold` 1..100; интервалы задержек: при паре — клампы+swap при min>max; при одном
|
||||
конце — кламп относительно сохранённого другого конца (L80–109).
|
||||
- Bool: JSON-булево (строки не «питон-булеватся»). String: `targetCurrency` → Upper;
|
||||
`aiProvider` вне списка провайдеров → пропуск; остальные — строка как есть.
|
||||
- List: только список → строки, срез 200. Dict: `colState` — как есть (Ruling 9).
|
||||
- `myPrompts`: ≤100; name≤80, prompt≤8000, description≤300 (trim); пустые name/prompt — дроп;
|
||||
id ≤40 или генерация `pp_` + 8 hex (Ruling дефолта, референс L143–160).
|
||||
- `aiConfigs`: только существующие провайдеры; `baseUrl`/`model` — строки; `apiKey` непустой,
|
||||
≥8 симв., без префикса `enc:` → шифруется (L161–175).
|
||||
- `tgKeys`: `apiId` — только цифры, длина 6..9 (5<len<10); `apiHash` ≥16 симв. → шифруется
|
||||
(L176–185).
|
||||
- Побочные эффекты PATCH: при `rateSource` — запуск `RatesService.RefreshAsync` (fire-and-forget);
|
||||
при `targetCurrency`/`conversionOn` — в этапе 2 ничего (нет leads; этап 3) (L186–192).
|
||||
|
||||
**Источники:** api-map §4.6 L147, L340–341; `settings_routes.py` целиком; `crypto.py`.
|
||||
|
||||
**Acceptance:** `dotnet test` — SettingsServiceTests PASS: снимок дефолтов; маскирование ключа;
|
||||
каждый кламп; swap интервалов; `myPrompts` clean+id; шифрование aiConfigs/tgKeys (в БД `enc:`);
|
||||
неизвестный ключ игнорируется. Отчёт: `task-3-report.md`.
|
||||
|
||||
### Task 4: KV-адаптер SettingsStore (EF) и DI
|
||||
|
||||
**Files:**
|
||||
- Create: `I/Persistence/Repositories/SettingsStore.cs` — реализует `ISettingsStore` на
|
||||
`TenantDbContext.Settings` (сущность `TenantSettingEntity` уже есть): чтение всех строк,
|
||||
сериализация/десериализация значений в JSON, `updated_at` — UTC-now.
|
||||
- Modify: `I/ServiceCollectionExtensions.cs` — `AddScoped<ISettingsStore, SettingsStore>()`;
|
||||
регистрация `ISecretCipher` из Task 1, `RatesService`-зависимостей из Tasks 6–8.
|
||||
- Modify: `S/SettingsModuleRegistrar.cs` (Create) — `AddSettingsModule()`: `SettingsService`,
|
||||
`RatesService`, `IncomingRules` (scoped); вызывается в `A/Program.cs` (Task 5).
|
||||
- Modify: `A/Deal.Api.csproj` — ProjectReference на `Deal.Modules.Settings`.
|
||||
|
||||
**Источники:** эталон: `I/Persistence/Repositories/AuthStore.cs`, `TenantModuleRegistrar.cs`,
|
||||
`ServiceCollectionExtensions.cs` (этап 1).
|
||||
|
||||
**Acceptance:** build 0/0; psql-проверка: GET через сервис на пустой схеме тенанта возвращает
|
||||
дефолты, `SetAsync` создаёт строку с `value_json`. Отчёт: `task-4-report.md`.
|
||||
|
||||
### Task 5: Эндпоинты GET/PATCH /api/settings + DI + curl-приёмка
|
||||
|
||||
**Files:**
|
||||
- Create: `A/Endpoints/SettingsEndpoints.cs` (`MapSettingsEndpoints`): `GET /api/settings` →
|
||||
PublicSettingsDto; `PATCH /api/settings` — тело произвольный JSON-объект →
|
||||
полный снимок после применения. Авторизация — через `SessionMiddleware`/`CurrentUser`
|
||||
(эталон `AuthEndpoints.cs`), 401 `{"detail":"Требуется авторизация"}`.
|
||||
- Modify: `A/Program.cs` — `AddSettingsModule()`, map групп эндпоинтов.
|
||||
- Модификации предыдущих задач собираются здесь же (порядок исполнения: T1→T4 затем T5).
|
||||
|
||||
**Контракт (api-map §3.4 L146–147, §4.6):** GET — все ключи §4.6 (camelCase, дефолты, маски,
|
||||
`providers` список); PATCH — те же поля-группы, что шлёт фронт (L340), ответ — полный снимок.
|
||||
Ошибок-исключений нет (мягкая семантика: невалидное поле просто не применяется).
|
||||
|
||||
**Acceptance (curl, cookie-сессия admin/admin):**
|
||||
1. `GET /api/settings` → дефолты: `aiEnabled:true, mlEnabled:true, minLen:24,
|
||||
archiveAfterDays:14, stopPhrases:[4 дефолтные], wantedType:"both", rateSource:"cbr",
|
||||
aiProvider:"deepseek", tgKeys:{apiId:"", apiHashSet:false}, colState:{}`, `providers` — 7.
|
||||
2. `PATCH {"archiveAfterDays":99,"minLen":3,"discJoinDelayMin":700,"discJoinDelayMax":5}` →
|
||||
в ответе `archiveAfterDays:30, minLen:10, discJoinDelayMin:5, discJoinDelayMax:700` (swap).
|
||||
3. `PATCH {"myPrompts":[{name:"x",prompt:"y"},{name:"",prompt:""}]}` → 1 элемент, `id` начинается `pp_`.
|
||||
4. `PATCH {"aiConfigs":{"deepseek":{"apiKey":"sk-1234567890ab"}}}` → ответ `keySet:true,
|
||||
keyMasked:"sk-1…90ab"`; psql: `value_json` содержит `enc:` (см. Task 7-контракт psql).
|
||||
5. `PATCH {"tgKeys":{"apiId":"123456","apiHash":"abcdefghijklmnop"}}` → `apiHashSet:true`.
|
||||
6. Неизвестный ключ `{"foo":1}` — без ошибки, снимок без `foo`.
|
||||
Отчёт: `task-5-report.md`.
|
||||
|
||||
### Task 6: ИИ-провайдеры и POST /api/ai/check (проверка подключения)
|
||||
|
||||
**Files:**
|
||||
- Create: `S/Application/IAiConnectionChecker.cs` — `Task<AiCheckResultDto> CheckAsync(
|
||||
AiCheckRequest request, ct)`, `S/Application/Models/AiCheckResultDto.cs` (Ok, Message, Provider,
|
||||
Name, Base, Model, Local, KeySet, KeyMasked), `AiCheckRequest` (ProviderId, BaseUrl, Model,
|
||||
ApiKey, IsLocal, ApiStyle).
|
||||
- Create: `I/Integrations/AiConnectionChecker.cs` — HTTP-реализация (Ruling 7) через
|
||||
`IHttpClientFactory` (таймаут 12 с), переиспользует формат сообщений прототипа.
|
||||
- Create: `A/Endpoints/AiCheckEndpoint.cs` (`MapAiCheckEndpoint`) — читает активную конфигурацию
|
||||
провайдера из `ISettingsStore` (расшифровка ключа через `ISecretCipher`), вызывает checker,
|
||||
отдаёт `{ok,message,provider,name,base,model,local,keySet,keyMasked}` (api-map §4.10 L365).
|
||||
- Test: `T/AiConnectionCheckerTests.cs` (fake `HttpMessageHandler`): без ключа; local; 200;
|
||||
401; 403; HTTP 500; сетевая ошибка.
|
||||
|
||||
**Источники:** `settings_routes.py` L195–219; `ai.py` L36–58 (provider_status + mask_key).
|
||||
|
||||
**Acceptance:** build 0/0; тесты PASS. curl: без ключа → `{"ok":false,"message":"Не задан
|
||||
API-ключ",...}`; провайдер `ollama` → ok:true «Локальный сервер…»; `deepseek` с неверным ключом
|
||||
и недоступным хостом → `"Ошибка соединения: …"` (сеть недоступна — допустимо). Отчёт:
|
||||
`task-6-report.md`.
|
||||
|
||||
### Task 7: Промпты и «Мои промпты» — интеграционная проверка границы с фронтом
|
||||
|
||||
Бэкенд-логика уже в Tasks 2–3 (`DefaultPrompts`, валидация `myPrompts`). Задача — контроль
|
||||
1:1 границы и приёмочные проверки (библиотека по сферам — фронтовая, `data.js` PROMPT_LIBRARY
|
||||
L180–200; `PromptLibraryModal.vue` не ходит в API; наружу идут только промпты-строки и
|
||||
`myPrompts`).
|
||||
|
||||
**Files:**
|
||||
- Test: `T/PromptDefaultsTests.cs` — дефолтные тексты начинаются/содержат маркеры из
|
||||
`data.js` (например `aiPrompt` содержит «Ты — классификатор входящих сообщений» и
|
||||
плейсхолдеры `{domain}`/`{keywords}`; `cardPrompt` — «О заявке»; `aiFilterPrompt` — «страж
|
||||
входящих»); `fill_prompt`-подстановка (аналог `ai.fill_prompt` L63–77): пустой domain →
|
||||
фраза-фолбэк, keywords склейка, ≤60 ключей.
|
||||
- Create: `S/Application/PromptFiller.cs` — подстановка `{domain}`/`{keywords}` (чистая функция,
|
||||
используется этапом 6 для ИИ-вызовов).
|
||||
|
||||
**Acceptance (curl):** 1) PATCH `aiPrompt` с плейсхолдерами → GET возвращает тот же текст;
|
||||
2) PATCH `myPrompts` 3 записи → GET отдаёт их (camelCase `id/name/description/prompt`);
|
||||
3) «Применить из библиотеки» фронта = локальная операция — API не вызывается. `dotnet test`
|
||||
PromptDefaultsTests PASS. Отчёт: `task-7-report.md`.
|
||||
|
||||
### Task 8: Курсы валют — сервис, кэш, эндпоинты /api/rates*
|
||||
|
||||
**Files:**
|
||||
- Create: `S/Application/IRatesSource.cs` — порт: `Task<Dictionary<string,double>?> FetchAsync(ct)`
|
||||
(курсы к RUB). `S/Application/Models/RatesDto.cs` — record `{Base, Rates, Source, UpdatedAtMs?}`.
|
||||
- Create: `S/Application/RatesService.cs` — `GetAsync(ct)` (кэш `ratesCache`; нет кэша → дефолт
|
||||
MockRates/source "mock"/updatedAt null); `RefreshAsync(ct)` (source из настройки: mock →
|
||||
сохранить MockRates; cbr → `IRatesSource`; неуспех → `false`, кэш не трогаем); `ShouldFetch(ct)`
|
||||
(нет кэша / смена источника / ≥6 ч, `rates.py` L77–83); `ConvertAmount(amount, fromCur, toCur)`
|
||||
— USDT→USD (L86–103). Ленивое обновление на GET при `ShouldFetch` — фоновый запуск
|
||||
`RefreshAsync`, ответ — текущий кэш.
|
||||
- Create: `I/Integrations/CbrRateSource.cs` — HTTP GET `https://www.cbr-xml-daily.ru/daily_json.js`
|
||||
(JSON), `Valute[code].Value/Nominal`, `RUB:1`; сбой → null (лог) (`rates.py` L43–59).
|
||||
- Create: `A/Endpoints/RatesEndpoints.cs` (`MapRatesEndpoints`): `GET /api/rates` → RatesDto;
|
||||
`POST /api/rates/refresh` → `{ok, rates: RatesDto}` (ok=false при сбое cbr; при mock — true).
|
||||
- Modify: `A/Program.cs` — map; DI: `IRatesSource→CbrRateSource` (scoped), `AddHttpClient`.
|
||||
- Test: `T/RatesServiceTests.cs` (fake store+source): mock-режим; cbr успех/сбой; ShouldFetch
|
||||
(интервал 6 ч, смена источника); ConvertAmount USDT=USD, отсутствующая валюта → null.
|
||||
|
||||
**Источники:** `services/rates.py` целиком; api-map §3.4 L149–150; `settings_routes.py` L224–232.
|
||||
|
||||
**Acceptance:** build 0/0; тесты PASS. curl: `PATCH {"rateSource":"mock"}` затем
|
||||
`POST /api/rates/refresh` → `{ok:true, rates:{base:"RUB", rates:{RUB:1,USD:92.5,…},
|
||||
source:"mock", updatedAt:<ms>}}`; `GET /api/rates` — тот же кэш. Отчёт: `task-8-report.md`.
|
||||
|
||||
### Task 9: ML-панель — порт IMlClient, детерминированная заглушка, эндпоинты /api/ml
|
||||
|
||||
**Files:**
|
||||
- Create: `C/Integrations/IMlClient.cs` + `C/Integrations/Models/*.cs` — record-DTO:
|
||||
`MlServiceStatusDto {Ready, Classes(Dictionary<string,double>), Learned, Eval{MlEvalDto}}`,
|
||||
`MlEvalDto {Count, Correct, Accuracy}`, `MlPredictResultDto {Take, Label?, Scores, Hits,
|
||||
Ready, Margin?, Terms[], Type?}`, `MlStatusResponseDto {Enabled, Service, Reachable, Stats{
|
||||
MlStatsDto}}`, `MlStatsDto {Ml, Ai, Learning, Ready, Classes, Learned, Reachable, Outbox}`
|
||||
(поля/типы 1:1 `ml_routes.py` L70–75 + `ml_client.snapshot()` L138–150).
|
||||
- Create: `I/Integrations/LocalMlClient.cs` — заглушка Ruling 5 (детерминированная; обучение
|
||||
недоступно до этапа 3 — модель всегда «не готова»; счётчики `mlDecisions/aiDecisions` —
|
||||
из KV settings, Ruling 1).
|
||||
- Create: `A/Endpoints/MlEndpoints.cs` (`MapMlEndpoints`):
|
||||
- `GET /api/ml/status` → `MlStatusResponseDto` (`enabled` = `mlEnabled !== false`);
|
||||
- `POST /api/ml/reset` → `{ok:true}` (мягкая ошибка `{ok:false,error}` — зарезервирована);
|
||||
- `POST /api/ml/predict` `{text}`: trim <2 симв. → 400 «Введите текст»; ответ
|
||||
`{text:<первые 200>, take, label, scores, hits, ready, margin, terms, type}`;
|
||||
- `POST /api/ml/candidates` `{dialogId, limit=10 (clamp 1..60)}` → `{items: []}` (данных
|
||||
telegram нет — этап 6; контракт §3.7 L196);
|
||||
- `POST /api/ml/apply` `{dialogId, msgId, action}` → 404 «Исходное сообщение не найдено»
|
||||
(нет сообщений до этапов 3/6; ветка `skip` — этап 6; контракт §3.7 L197).
|
||||
- НЕ реализуем: `ml/learn`, `ml/flush` (фронт не вызывает, api-map п.9).
|
||||
- Modify: `A/Program.cs` — DI `IMlClient→LocalMlClient` (scoped), map.
|
||||
- Test: `T/LocalMlClientTests.cs` (status-форма; predict неготовой модели — все поля; reset → ok).
|
||||
|
||||
**Источники:** api-map §3.7, §4.10 L363; `ml_routes.py` L66–91, L112–171; `ml_client.py` L127–150;
|
||||
`mlservice/model.py` (predict L184–293, status L325–345 — эталон полей для этапа 6).
|
||||
|
||||
**Acceptance:** build 0/0; тесты PASS. curl: login → `GET /api/ml/status` (все поля, `reachable:
|
||||
true`, `ready:false`, `stats.outbox:0`); `POST /api/ml/predict {"text":"x"}` → 400;
|
||||
`POST /api/ml/predict {"text":"Python backend на fastapi, бот в телеграм"}` → `take:false,
|
||||
label:null, scores:{}, ready:false`; `POST /api/ml/reset` → `{ok:true}`; `POST /api/ml/candidates`
|
||||
→ `{"items":[]}`. Отчёт: `task-9-report.md`.
|
||||
|
||||
### Task 10: Тестер фильтров — этап-1 правила и POST /api/admin/check-message
|
||||
|
||||
**Files:**
|
||||
- Create: `S/Application/IncomingRules.cs` — чистая реализация `stage1_plain` (`pipeline.py`
|
||||
L94–124) поверх `ISettingsStore`: минимальная длина (`minLen`), стоп-фразы (casefold, ответ —
|
||||
конкретная фраза), блокировка резюме (`blockResumes` + `resumeMarkers` с контекстным guard
|
||||
«вакансия… присылайте резюме» — не режем, `pipeline._resume_reason` L644–654), тип заявки
|
||||
(`wantedType` + маркеры найма `hireMarkers`); результат
|
||||
`{pass, reason, stage:1, kind:length|stop|resume|type, kw}`.
|
||||
- Create: `A/Endpoints/FilterTesterEndpoints.cs` (`MapFilterTesterEndpoints`):
|
||||
`POST /api/admin/check-message` `{text}` → `{stage1:{pass,reason}, stage2, passed}`
|
||||
(1:1 `dashboard_routes.py` L267–284): если этап-1 не прошёл → `stage2:{pass:false,reason:null,
|
||||
skipped:true}, passed:false`; иначе `stage2:{pass:true,reason:null,skipped:true}` — ИИ-фильтр
|
||||
на этапе 2 всегда skipped (Ruling 4/8; реальный ИИ-фильтр — этап 6).
|
||||
- Test: `T/IncomingRulesTests.cs`: короткий текст; стоп-фраза из настроек; резюме (маркер);
|
||||
guard «…вакансия… присылайте резюме» → pass; `wantedType:"freelance"` с вакансионным маркером;
|
||||
`wantedType:"vacancy"` с разовым заказом.
|
||||
|
||||
**Источники:** api-map §3.2 L109, §4.10 L364; `dashboard_routes.py` L267–284; `pipeline.py`
|
||||
L94–124; фронт: `SettingsView.vue` L142–155 (тестер), `store.js` L1723–1725.
|
||||
|
||||
**Acceptance:** build 0/0; тесты PASS. curl: с дефолтами текст «Заработок на крипте…» (длина
|
||||
≥24, без стоп-фраз) → `stage1.pass:true, stage2.skipped:true, passed:true`; текст «Ищу работу
|
||||
python» → `stage1.pass:false, kind:"resume"` (если ≥minLen); «взаимный пиар» внутри → `kind:
|
||||
"stop"`. Отчёт: `task-10-report.md`.
|
||||
|
||||
### Task 11: Финал этапа — интеграция и сквозная приёмка
|
||||
|
||||
- `scripts/build.sh`/`scripts/test.sh` — успешны; `dotnet build Deal.sln` 0/0; все unit-тесты PASS.
|
||||
- Сквозной curl-сценарий Settings-экрана: login admin/admin → GET /settings →
|
||||
PATCH-группы из Tasks 5/7 (обработка, ИИ-промпты, myPrompts, aiConfigs, tgKeys, валюта,
|
||||
хранение/уведомления: `autoArchive/archiveAfterDays/remindersEnabled`, colState) →
|
||||
POST /ai/check → GET /rates + POST /rates/refresh (mock) → GET /api/ml/status + predict +
|
||||
reset → POST /api/admin/check-message (pass и отсев).
|
||||
- psql-проверка схемы дефолтного тенанта (`SET search_path TO tenant_00000000000000000000000000000001;`):
|
||||
строки settings созданы, `value_json` для aiConfigs/tgKeys содержит `enc:` и не содержит
|
||||
открытого ключа; внутренние ключи (`ratesCache`, `mlDecisions`) не появляются в GET /settings.
|
||||
- Известные ограничения этапа (зафиксировать в отчёте): Telegram-вкладка, кнопки «Проверить
|
||||
правила сейчас»/«Пересобрать индекс» (admin/tick, admin/fts), «Предложить ключи»
|
||||
(ai/suggest-keywords) и весь канбан-фронт не работают до этапов 3–6 (Ruling 8/11).
|
||||
- Обновить `docs/technical/Техническая-документация-Дейл.md` (раздел настроек: env
|
||||
`DEAL_ENCRYPTION_KEY`, поведение GET/PATCH /settings, креды).
|
||||
- Отчёт `task-11-report.md` + финальная строка в `progress.md`.
|
||||
|
||||
## Self-Review
|
||||
|
||||
1. **Spec coverage:** ТЗ §8 (настройки тенанта) — Tasks 1–10; §5 (этап-1 фильтры/тип/резюме —
|
||||
только настройки+тестер) — Task 10, (ML/ИИ-слои пайплайна — этапы 4/6, вне); §7 (обработка) —
|
||||
вне (этап 4); §9 (лимиты) — вне; api-map §3.4 — Tasks 5/7/8; §3.7 — Task 9; admin/check-message
|
||||
— Task 10; §4.6/4.7 — Tasks 2/3/5/7; tgKeys-часть §4.6 — Task 3/5; шифрование §8 архитектуры —
|
||||
Task 1.
|
||||
2. **Placeholder scan:** конкретные адаптеры и контракты; «заглушки» только там, где разрешено
|
||||
решением владельца (п.5): `LocalMlClient` (Task 9), ИИ-фильтр в тестере = skipped (Task 10);
|
||||
референсы на строки файлов точные. FIXME/TODO нет.
|
||||
3. **Type consistency:** один модуль Settings владеет каталогом ключей/дефолтами — Kanban/Pipeline
|
||||
(этапы 3/4) читают те же ключи через `ISettingsStore`; `IMlClient`-контракт (Contracts)
|
||||
един для панели (этап 2), счётчиков (этап 3) и предсказаний (этап 4); сущность
|
||||
`TenantSettingEntity` не меняется; схемы/миграции не добавляются.
|
||||
4. **Вне scope этапа 2:** канбан-колонки/карточки/архив-корзина и их эндпоинты (этап 3),
|
||||
pipeline/очередь/отсев/дедуп (этап 4), projects/напоминания-отложенные/файлы (этап 5),
|
||||
реальные ml/ai/telegram-сервисы и /api/tg/* (этап 6), discovery, оператор/инвайты/лимиты/
|
||||
аудит (этап 7); colState-эндпоинты; библиотека промптов (фронтовая); звук/вид (фронт).
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,71 +1,71 @@
|
||||
# Дейл (Deal) — Этап 9: единая карточка (unified card) Implementation Plan
|
||||
|
||||
> Исторический документ этапа 9. Актуальное состояние — `docs/superpowers/STATUS.md` и `docs/technical/Техническая-документация-Дейл.md`.
|
||||
|
||||
**Goal:** Устранить дуальность «карточка канбана / проектная карточка». Одна сущность **карточка**
|
||||
(ядро id/title/source + опциональные модули) работает во всех дашбордах; «лид» как понятие и
|
||||
`ProjectCards`-дублирование упраздняются; колонки/стадии/зоны — единый контейнер с политиками.
|
||||
Бэк (C#) и фронт (Vue) переписываются на единую модель; данные тестовые, схема пересоздаётся.
|
||||
|
||||
**Spec:** `docs/architecture/2026-09-09-unified-card.md`; ТЗ: `docs/spec/ТЗ-дейл-новая-архитектура.md`
|
||||
(термины §2, карточка §5.5, канбаны §6); код: модули Kanban/Projects/Pipeline, Deal.Infrastructure
|
||||
(миграции/адаптеры), Deal.Api (LeadsEndpoints/ProjectsEndpoints/PipelineEndpoints), фронт
|
||||
`store/{leads,projects}.js`, компоненты LeadCard/ProjectCard/LeadDrawer/ProjectDrawer/Column/ProjectColumn.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Проект **НЕ git**; фиксация — отчёты `task-N-report.md` и `progress.md` в `.superpowers/sdd/deal-stage9-unified-card/`.
|
||||
- .NET 10; sln собираются 0 warnings/0 errors; dev-Postgres `deal-postgres` (:5433); системные миграции —
|
||||
`dotnet ef database update --context DealDbContext` из `src/core`; tenant-миграции — провижинер на старте.
|
||||
- Код-стайл: 1 тип = 1 файл; XML-doc на public; русские комментарии; без регионов; без магических чисел;
|
||||
времена `DateTimeOffset` (UTC); JSON camelCase; ошибки API — `{detail}`.
|
||||
- Фронт: Vue 3 + чистый JS, без TS/роутера; Composition API; `npm run build` зелёный после каждого шага.
|
||||
- Тесты: core `Deal.Tests.Unit` (1139), telegram 118, ai 52, ml 38 — прогон после каждой фазы.
|
||||
- Секреты — только env (`DEAL_*`).
|
||||
- Вне рамок: Kafka, k8s, саморегистрация, «третий» дашборд (архитектура готова, реализация — позже).
|
||||
|
||||
## Ключевые решения (Rulings этапа)
|
||||
|
||||
- **R1 — единый агрегат карточки.** Ядро `Card { Id, Title, Source }`; модули-роли (контент, бюджет,
|
||||
контакты, атрибуты, комментарии, ссылки, файлы, ТЗ, история, напоминание, размещение) — опциональные
|
||||
части агрегата (jsonb/колонки одной таблицы), а не классы-наследники. Вид = композиция модулей.
|
||||
- **R2 — Source.** `ISource` + варианты: Local/Web/File/Telegram/Row/Api/Ai/Composite (Origin+Pipeline).
|
||||
У карточки из пайплайна — `Composite(Origin: Telegram, Pipeline: [Ai/ML])`.
|
||||
- **R3 — единый контейнер.** Одна таблица/реестр контейнеров (kind: inbox/board/stage/archive/trash/
|
||||
terminal), политики — роли (`IContainerPolicy`), не enum-свойства. Стадии «Выбранных» — контейнеры
|
||||
kind=stage (предзаданный каталог), доски — kind=board (создаёт пользователь/ИИ).
|
||||
- **R4 — переход.** Один `ICardMover.MoveAsync(card, toContainerId, ctx)`; «взять в работу» = переход в
|
||||
контейнер planned той же карточки (никакого `col=taken` + клона в ProjectCards); «Выбранные → архив/
|
||||
корзина дашборда» запрещено политикой пространства; терминальные зоны — политика.
|
||||
- **R5 — API.** `/api/cards` + `/api/containers` (единый контракт); `/api/leads`, `/api/projects`
|
||||
упраздняются; фронт переписывается. SSE-события переходят на карточки.
|
||||
- **R6 — пайплайн.** Создаёт карточку (не «лид»): `CardComposer` → `ICardStore.Add`; дедуп/отсев/ML/ИИ
|
||||
не знают «лидов». Названия в коде/БД: lead→card, project card→card in stage-container.
|
||||
|
||||
## Задачи этапа
|
||||
|
||||
- **T1. Доменные контракты единой карточки (C#)** — модуль Cards: ICard/ICard<TSource>, ISource-иерархия,
|
||||
модули-роли, IContainer/IContainerPolicy, ICardMover; реестры (контейнеры по умолчанию, стадии,
|
||||
SourceKind). Без изменения поведения текущих модулей (новые типы + тесты чистых правил).
|
||||
- **T2. EF-модель и миграция** — одна таблица `Cards` (общие поля + jsonb-модули + source + container_id),
|
||||
таблица `Containers` (доски/стадии/зоны), удаление ProjectCards/LeadComments-дублей; системная и
|
||||
tenant-миграции; провижининг контейнеров по умолчанию.
|
||||
- **T3. Адаптер ICardStore** — единый EF-адаптер (слияние KanbanStore/ProjectStore), чтение/запись
|
||||
карточки целиком (jsonb-модули), контейнеры, атомарные append (комментарии/ссылки/файлы), move с
|
||||
историей/напоминаниями.
|
||||
- **T4. Сервисы карточек/контейнеров** — CardsService (переходы, правила колонок, обучение ML),
|
||||
ContainersService (CRUD колонок, принятие ИИ-предложений, reorder), перенос логики Projects
|
||||
(файлы/ТЗ/напоминания/история) в модули карточки.
|
||||
- **T5. Pipeline** — создание карточки через ICardStore; терминология; дедуп на карточку.
|
||||
- **T6. API единый** — `/api/cards` и `/api/containers`; SSE; удаление старых ручек; интеграционные
|
||||
тесты/curl-приёмка.
|
||||
- **T7. ML-сервис/контракты** — обучение на действиях с карточками (колонки/стадии едино), без «lead».
|
||||
- **T8. Фронт: store** — единый слайс карточек/контейнеров вместо leads.js+projects.js; API-клиент.
|
||||
- **T9. Фронт: компоненты** — единые LeadCard-база→Card, Column/ProjectColumn→ContainerColumn,
|
||||
LeadDrawer/ProjectDrawer→CardDrawer; экраны Дашборд/«Выбранные» — один канбан по пространству.
|
||||
- **T10. Финал** — сквозная приёмка, доки (ТЗ/техдок/api-map), чистка, ledger.
|
||||
|
||||
## Порядок и зависимости
|
||||
|
||||
T1 → T2 → T3 → (T4, T5) → T6 → T7 → (T8, T9) → T10. Каждая задача завершается зелёной сборкой и
|
||||
прогоном тестов; API-контракт меняется один раз на T6 (до этого новые типы живут рядом со старыми).
|
||||
# Дейл (Deal) — Этап 9: единая карточка (unified card) Implementation Plan
|
||||
|
||||
> Исторический документ этапа 9. Актуальное состояние — `docs/superpowers/STATUS.md` и `docs/technical/Техническая-документация-Дейл.md`.
|
||||
|
||||
**Goal:** Устранить дуальность «карточка канбана / проектная карточка». Одна сущность **карточка**
|
||||
(ядро id/title/source + опциональные модули) работает во всех дашбордах; «лид» как понятие и
|
||||
`ProjectCards`-дублирование упраздняются; колонки/стадии/зоны — единый контейнер с политиками.
|
||||
Бэк (C#) и фронт (Vue) переписываются на единую модель; данные тестовые, схема пересоздаётся.
|
||||
|
||||
**Spec:** `docs/architecture/2026-09-09-unified-card.md`; ТЗ: `docs/spec/ТЗ-дейл-новая-архитектура.md`
|
||||
(термины §2, карточка §5.5, канбаны §6); код: модули Kanban/Projects/Pipeline, Deal.Infrastructure
|
||||
(миграции/адаптеры), Deal.Api (LeadsEndpoints/ProjectsEndpoints/PipelineEndpoints), фронт
|
||||
`store/{leads,projects}.js`, компоненты LeadCard/ProjectCard/LeadDrawer/ProjectDrawer/Column/ProjectColumn.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Проект **НЕ git**; фиксация — отчёты `task-N-report.md` и `progress.md` в `.superpowers/sdd/deal-stage9-unified-card/`.
|
||||
- .NET 10; sln собираются 0 warnings/0 errors; dev-Postgres `deal-postgres` (:5433); системные миграции —
|
||||
`dotnet ef database update --context DealDbContext` из `src/core`; tenant-миграции — провижинер на старте.
|
||||
- Код-стайл: 1 тип = 1 файл; XML-doc на public; русские комментарии; без регионов; без магических чисел;
|
||||
времена `DateTimeOffset` (UTC); JSON camelCase; ошибки API — `{detail}`.
|
||||
- Фронт: Vue 3 + чистый JS, без TS/роутера; Composition API; `npm run build` зелёный после каждого шага.
|
||||
- Тесты: core `Deal.Tests.Unit` (1139), telegram 118, ai 52, ml 38 — прогон после каждой фазы.
|
||||
- Секреты — только env (`DEAL_*`).
|
||||
- Вне рамок: Kafka, k8s, саморегистрация, «третий» дашборд (архитектура готова, реализация — позже).
|
||||
|
||||
## Ключевые решения (Rulings этапа)
|
||||
|
||||
- **R1 — единый агрегат карточки.** Ядро `Card { Id, Title, Source }`; модули-роли (контент, бюджет,
|
||||
контакты, атрибуты, комментарии, ссылки, файлы, ТЗ, история, напоминание, размещение) — опциональные
|
||||
части агрегата (jsonb/колонки одной таблицы), а не классы-наследники. Вид = композиция модулей.
|
||||
- **R2 — Source.** `ISource` + варианты: Local/Web/File/Telegram/Row/Api/Ai/Composite (Origin+Pipeline).
|
||||
У карточки из пайплайна — `Composite(Origin: Telegram, Pipeline: [Ai/ML])`.
|
||||
- **R3 — единый контейнер.** Одна таблица/реестр контейнеров (kind: inbox/board/stage/archive/trash/
|
||||
terminal), политики — роли (`IContainerPolicy`), не enum-свойства. Стадии «Выбранных» — контейнеры
|
||||
kind=stage (предзаданный каталог), доски — kind=board (создаёт пользователь/ИИ).
|
||||
- **R4 — переход.** Один `ICardMover.MoveAsync(card, toContainerId, ctx)`; «взять в работу» = переход в
|
||||
контейнер planned той же карточки (никакого `col=taken` + клона в ProjectCards); «Выбранные → архив/
|
||||
корзина дашборда» запрещено политикой пространства; терминальные зоны — политика.
|
||||
- **R5 — API.** `/api/cards` + `/api/containers` (единый контракт); `/api/leads`, `/api/projects`
|
||||
упраздняются; фронт переписывается. SSE-события переходят на карточки.
|
||||
- **R6 — пайплайн.** Создаёт карточку (не «лид»): `CardComposer` → `ICardStore.Add`; дедуп/отсев/ML/ИИ
|
||||
не знают «лидов». Названия в коде/БД: lead→card, project card→card in stage-container.
|
||||
|
||||
## Задачи этапа
|
||||
|
||||
- **T1. Доменные контракты единой карточки (C#)** — модуль Cards: ICard/ICard<TSource>, ISource-иерархия,
|
||||
модули-роли, IContainer/IContainerPolicy, ICardMover; реестры (контейнеры по умолчанию, стадии,
|
||||
SourceKind). Без изменения поведения текущих модулей (новые типы + тесты чистых правил).
|
||||
- **T2. EF-модель и миграция** — одна таблица `Cards` (общие поля + jsonb-модули + source + container_id),
|
||||
таблица `Containers` (доски/стадии/зоны), удаление ProjectCards/LeadComments-дублей; системная и
|
||||
tenant-миграции; провижининг контейнеров по умолчанию.
|
||||
- **T3. Адаптер ICardStore** — единый EF-адаптер (слияние KanbanStore/ProjectStore), чтение/запись
|
||||
карточки целиком (jsonb-модули), контейнеры, атомарные append (комментарии/ссылки/файлы), move с
|
||||
историей/напоминаниями.
|
||||
- **T4. Сервисы карточек/контейнеров** — CardsService (переходы, правила колонок, обучение ML),
|
||||
ContainersService (CRUD колонок, принятие ИИ-предложений, reorder), перенос логики Projects
|
||||
(файлы/ТЗ/напоминания/история) в модули карточки.
|
||||
- **T5. Pipeline** — создание карточки через ICardStore; терминология; дедуп на карточку.
|
||||
- **T6. API единый** — `/api/cards` и `/api/containers`; SSE; удаление старых ручек; интеграционные
|
||||
тесты/curl-приёмка.
|
||||
- **T7. ML-сервис/контракты** — обучение на действиях с карточками (колонки/стадии едино), без «lead».
|
||||
- **T8. Фронт: store** — единый слайс карточек/контейнеров вместо leads.js+projects.js; API-клиент.
|
||||
- **T9. Фронт: компоненты** — единые LeadCard-база→Card, Column/ProjectColumn→ContainerColumn,
|
||||
LeadDrawer/ProjectDrawer→CardDrawer; экраны Дашборд/«Выбранные» — один канбан по пространству.
|
||||
- **T10. Финал** — сквозная приёмка, доки (ТЗ/техдок/api-map), чистка, ledger.
|
||||
|
||||
## Порядок и зависимости
|
||||
|
||||
T1 → T2 → T3 → (T4, T5) → T6 → T7 → (T8, T9) → T10. Каждая задача завершается зелёной сборкой и
|
||||
прогоном тестов; API-контракт меняется один раз на T6 (до этого новые типы живут рядом со старыми).
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
# Дейл (Deal) — Этап 10: оператор-консоль, аналитика и аудит действий
|
||||
|
||||
> Исторический документ этапа 10. Актуальное состояние — `docs/superpowers/STATUS.md` и `docs/technical/Техническая-документация-Дейл.md`.
|
||||
|
||||
**Goal:** закрыть SaaS-контур снаружи: UI операторской админки и страница активации инвайта; сквозной
|
||||
аудит (входы/выходы/действия пользователей); аналитика расхода токенов; дашборды по логам (ELK/Loki).
|
||||
|
||||
**Контекст:** этапы 0–9 завершены. Операторский API уже есть (`/api/operator/*`: auth, tenants, invites,
|
||||
limits, audit, health; `/api/join`), но **UI отсутствует**. Аудит (`public.audit_log`, append-only) покрывает
|
||||
SaaS-события (входы, инвайты, тенанты, лимиты, impersonation), но **не покрывает выходы и действия
|
||||
тенант-пользователей**. Расход токенов хранится агрегатом (`public.tenant_limits.UsedTokens`), **истории нет**.
|
||||
|
||||
## Решения этапа
|
||||
|
||||
- **A1. Роутинг фронта.** Проект без vue-router. Ввести минимальный hash-роутер: `#/` — основное
|
||||
приложение (как сейчас), `#/operator` — консоль, `#/join?code=…` — активация инвайта. Без новых зависимостей.
|
||||
- **A2. Аудит — единая точка.** Только `AuditService` пишет в `public.audit_log` (append-only).
|
||||
Действия тенант-пользователей пишутся оттуда же (actor=tenant). Секреты не логируются.
|
||||
- **A3. Расход токенов — событийная история.** Новая таблица `public.token_usage_events`
|
||||
(time-series: тенант, время, провайдер, модель, вид (ai|ml), токены). Агрегат `tenant_limits`
|
||||
остаётся для гейта; история — для аналитики.
|
||||
- **A4. Аналитика — операторские read-only эндпоинты** под `/api/operator/analytics/*`; никаких
|
||||
изменений существующих контрактов (только расширение `/api/operator/audit` пагинацией/фильтром actorId).
|
||||
- **A5. ELK.** Логи структурированы Serilog JSON. Аналитика по логам — Grafana/Loki: provisioning
|
||||
datasource + дашборды (входы/выходы/неудачные входы, ошибки, RPS, действия).
|
||||
|
||||
## Задачи
|
||||
|
||||
- **T1. Аудит действий (бэк).** Дополнить `AuditEvents`: `tenant_logout`, `operator_logout`,
|
||||
`invite_joined` (активация/join), действия карточек (`card_created`, `card_moved`, `card_trashed`,
|
||||
`card_restored`, `card_deleted`, `card_comment_added`), контейнеры (`container_created`,
|
||||
`container_updated`, `container_deleted`), настройки (`settings_updated`), каналы
|
||||
(`channel_enabled`/`channel_created`), Telegram (`telegram_linked`). Записать в соответствующих
|
||||
сервисах/эндпоинтах (без секретов). Войти обязаны: logout тенанта и оператора.
|
||||
- **T2. История расхода токенов (бэк).** Таблица `public.token_usage_events` + EF-конфигурация +
|
||||
системная миграция. Запись события в точке списания токенов (AI- и ML-путь). Порт для чтения
|
||||
агрегатов/серий.
|
||||
- **T3. Аналитика (бэк).** `/api/operator/analytics/overview`, `/tokens`, `/activity`; расширить
|
||||
`/api/operator/audit` (offset/пагинация, actorId, total). Контракт:
|
||||
`docs/architecture/2026-09-10-operator-analytics-contract.md`.
|
||||
- **T4. Оператор-консоль (фронт).** Hash-роутер; экраны: вход оператора, тенанты (список/создать/
|
||||
suspend/resume/impersonate), инвайты (создать/отозвать/ссылка), лимиты (список/правка), аудит-лента
|
||||
(фильтры/пагинация), аналитика (обзор/токены/действия).
|
||||
- **T5. Страница активации (фронт).** `#/join?code=…` → форма (email/имя/пароль) → `POST /api/join`.
|
||||
- **T6. Наблюдаемость (ELK/Loki).** Grafana provisioning (datasource Loki + дашборды), promtail-лейблы;
|
||||
дашборды: входы/выходы/неудачные входы, ошибки 5xx, RPS, действия пользователей.
|
||||
- **T7. Приёмка/доки.** Сквозная проверка (operator → tenant → invite → join → действия → аудит/аналитика),
|
||||
обновить `docs/api`, `docs/technical`, `docs/user-guide`, `docs/superpowers/STATUS.md`.
|
||||
|
||||
## Границы
|
||||
|
||||
- Kafka/k8s/биллинг/саморегистрация — вне рамок.
|
||||
- Реальные Telegram/LLM-креды — не требуются (аналитика токенов наполняется на любых AI/ML-вызовах).
|
||||
- Данные тестовые; схема system (`public`) расширяется одной миграцией.
|
||||
|
||||
## Порядок
|
||||
|
||||
T1+T2+T3 (бэк, контракт) → T4+T5 (фронт по контракту) → T6 (наблюдаемость, параллельно) → T7 (приёмка).
|
||||
|
||||
Каждая задача: `dotnet build Deal.sln` 0/0, core-тесты зелёные, `npm run build` зелёный.
|
||||
# Дейл (Deal) — Этап 10: оператор-консоль, аналитика и аудит действий
|
||||
|
||||
> Исторический документ этапа 10. Актуальное состояние — `docs/superpowers/STATUS.md` и `docs/technical/Техническая-документация-Дейл.md`.
|
||||
|
||||
**Goal:** закрыть SaaS-контур снаружи: UI операторской админки и страница активации инвайта; сквозной
|
||||
аудит (входы/выходы/действия пользователей); аналитика расхода токенов; дашборды по логам (ELK/Loki).
|
||||
|
||||
**Контекст:** этапы 0–9 завершены. Операторский API уже есть (`/api/operator/*`: auth, tenants, invites,
|
||||
limits, audit, health; `/api/join`), но **UI отсутствует**. Аудит (`public.audit_log`, append-only) покрывает
|
||||
SaaS-события (входы, инвайты, тенанты, лимиты, impersonation), но **не покрывает выходы и действия
|
||||
тенант-пользователей**. Расход токенов хранится агрегатом (`public.tenant_limits.UsedTokens`), **истории нет**.
|
||||
|
||||
## Решения этапа
|
||||
|
||||
- **A1. Роутинг фронта.** Проект без vue-router. Ввести минимальный hash-роутер: `#/` — основное
|
||||
приложение (как сейчас), `#/operator` — консоль, `#/join?code=…` — активация инвайта. Без новых зависимостей.
|
||||
- **A2. Аудит — единая точка.** Только `AuditService` пишет в `public.audit_log` (append-only).
|
||||
Действия тенант-пользователей пишутся оттуда же (actor=tenant). Секреты не логируются.
|
||||
- **A3. Расход токенов — событийная история.** Новая таблица `public.token_usage_events`
|
||||
(time-series: тенант, время, провайдер, модель, вид (ai|ml), токены). Агрегат `tenant_limits`
|
||||
остаётся для гейта; история — для аналитики.
|
||||
- **A4. Аналитика — операторские read-only эндпоинты** под `/api/operator/analytics/*`; никаких
|
||||
изменений существующих контрактов (только расширение `/api/operator/audit` пагинацией/фильтром actorId).
|
||||
- **A5. ELK.** Логи структурированы Serilog JSON. Аналитика по логам — Grafana/Loki: provisioning
|
||||
datasource + дашборды (входы/выходы/неудачные входы, ошибки, RPS, действия).
|
||||
|
||||
## Задачи
|
||||
|
||||
- **T1. Аудит действий (бэк).** Дополнить `AuditEvents`: `tenant_logout`, `operator_logout`,
|
||||
`invite_joined` (активация/join), действия карточек (`card_created`, `card_moved`, `card_trashed`,
|
||||
`card_restored`, `card_deleted`, `card_comment_added`), контейнеры (`container_created`,
|
||||
`container_updated`, `container_deleted`), настройки (`settings_updated`), каналы
|
||||
(`channel_enabled`/`channel_created`), Telegram (`telegram_linked`). Записать в соответствующих
|
||||
сервисах/эндпоинтах (без секретов). Войти обязаны: logout тенанта и оператора.
|
||||
- **T2. История расхода токенов (бэк).** Таблица `public.token_usage_events` + EF-конфигурация +
|
||||
системная миграция. Запись события в точке списания токенов (AI- и ML-путь). Порт для чтения
|
||||
агрегатов/серий.
|
||||
- **T3. Аналитика (бэк).** `/api/operator/analytics/overview`, `/tokens`, `/activity`; расширить
|
||||
`/api/operator/audit` (offset/пагинация, actorId, total). Контракт:
|
||||
`docs/architecture/2026-09-10-operator-analytics-contract.md`.
|
||||
- **T4. Оператор-консоль (фронт).** Hash-роутер; экраны: вход оператора, тенанты (список/создать/
|
||||
suspend/resume/impersonate), инвайты (создать/отозвать/ссылка), лимиты (список/правка), аудит-лента
|
||||
(фильтры/пагинация), аналитика (обзор/токены/действия).
|
||||
- **T5. Страница активации (фронт).** `#/join?code=…` → форма (email/имя/пароль) → `POST /api/join`.
|
||||
- **T6. Наблюдаемость (ELK/Loki).** Grafana provisioning (datasource Loki + дашборды), promtail-лейблы;
|
||||
дашборды: входы/выходы/неудачные входы, ошибки 5xx, RPS, действия пользователей.
|
||||
- **T7. Приёмка/доки.** Сквозная проверка (operator → tenant → invite → join → действия → аудит/аналитика),
|
||||
обновить `docs/api`, `docs/technical`, `docs/user-guide`, `docs/superpowers/STATUS.md`.
|
||||
|
||||
## Границы
|
||||
|
||||
- Kafka/k8s/биллинг/саморегистрация — вне рамок.
|
||||
- Реальные Telegram/LLM-креды — не требуются (аналитика токенов наполняется на любых AI/ML-вызовах).
|
||||
- Данные тестовые; схема system (`public`) расширяется одной миграцией.
|
||||
|
||||
## Порядок
|
||||
|
||||
T1+T2+T3 (бэк, контракт) → T4+T5 (фронт по контракту) → T6 (наблюдаемость, параллельно) → T7 (приёмка).
|
||||
|
||||
Каждая задача: `dotnet build Deal.sln` 0/0, core-тесты зелёные, `npm run build` зелёный.
|
||||
|
||||
@@ -1,71 +1,71 @@
|
||||
# Дейл (Deal) — Этап 11: Локализация интерфейса (i18n)
|
||||
|
||||
> Исторический документ этапа 11. Актуальное состояние — `docs/superpowers/STATUS.md` и `docs/technical/Техническая-документация-Дейл.md`.
|
||||
|
||||
> Статус: план (не начат). Требование владельца от 2026-09-10.
|
||||
> Связанные документы: `docs/superpowers/plans/2026-09-05-deal-roadmap.md` (Этап 11),
|
||||
> `docs/spec/ТЗ-дейл-новая-архитектура.md` (§11, локализация), `docs/superpowers/STATUS.md` (Заделы).
|
||||
|
||||
## Цель
|
||||
|
||||
Весь интерфейс — на русском; **все** пользовательские тексты вынесены в ресурсы (словари), чтобы
|
||||
можно было добавлять новые языки и менять язык **на лету**. Русский — язык по умолчанию.
|
||||
|
||||
## Требования
|
||||
|
||||
- **Русский по умолчанию.** Все видимые строки UI: экраны, кнопки, подписи, заголовки, пустые состояния,
|
||||
подсказки, тултипы, тексты подтверждений, уведомления/тосты, страницы оператора и активации инвайта.
|
||||
- **Без хардкода.** Ни одна пользовательская строка не хранится в компонентах/шаблонах напрямую —
|
||||
только ключ в словаре. Технические строки (id/ключи/логи) не локализуются.
|
||||
- **Ошибки API.** Ответы бэка остаются `{detail}` + HTTP-код; фронт показывает локализованный текст по
|
||||
коду/ключу ошибки (расширяемый словарь ошибок). При необходимости бэк отдаёт код ошибки, а не только текст.
|
||||
- **Переключение на лету.** Смена языка без перезагрузки страницы; выбранный язык сохраняется
|
||||
(localStorage/настройки пользователя) и восстанавливается при входе.
|
||||
- **Расширяемость.** Новый язык = новый файл словаря (+ регистрация), без правок компонентов.
|
||||
- **Форматирование.** Даты/время/числа/валюты — через i18n-форматтеры; плюрализация — по правилам языка.
|
||||
Бэкенд-форматирование human-меток («только что», «N мин») — перевести на клиентские форматтеры или ключи.
|
||||
- **Ключи.** Стабильные, сгруппированные по областям (`nav/`, `cards/`, `settings/`, `operator/`, `errors/`…).
|
||||
Отсутствующий ключ в языке → фолбэк на русский (и, при необходимости, лог о пропуске).
|
||||
|
||||
## Область
|
||||
|
||||
- Основное приложение: дашборд, «Выбранные», настройки (все вкладки), каналы, обработка/состояние, вход.
|
||||
- Оператор-консоль (этап 10): все разделы + страница активации инвайта.
|
||||
|
||||
## Объём (по факту кода на 2026-09-10)
|
||||
|
||||
- 69 `.vue` + 20 `.js`; ~708 строковых литералов на кириллице в ~67 файлах
|
||||
(components ≈478, views ≈300, store ≈82) + текст прямо в шаблонах.
|
||||
- Области: навигация/шапка, карточки и колонки, драйвер карточки, настройки (все вкладки), каналы,
|
||||
обработка/состояние, вход, оператор-консоль (все разделы), страница активации, тосты/подтверждения.
|
||||
|
||||
## Решение владельца (2026-09-10)
|
||||
|
||||
- На этом этапе — **только русский**. Переключатель языка и второй язык — **в бэклоге**: делаем, когда
|
||||
возникнет потребность (см. «Отложено» ниже).
|
||||
- Задача этапа — **вынести все строки в ресурсы**, чтобы язык можно было добавить позже без правок компонентов.
|
||||
- Визуал и тексты — **1:1 с текущими** (вынос не меняет отображаемый текст).
|
||||
|
||||
## Задачи
|
||||
|
||||
- **T1. i18n-ядро (без тяжёлых зависимостей).** Composable/модуль: `t(key, params)`, реактивный `locale`
|
||||
(значение по умолчанию `ru`), `setLocale()` (архитектурно готов, UI-переключателя нет), загрузка
|
||||
словарей, фолбэк на ru при отсутствии ключа. `src/i18n/` + `locales/ru.js`.
|
||||
- **T2. Инвентаризация и словарь ru.** Вынести все строки в `locales/ru.js`, ключи сгруппированы по
|
||||
областям (`common/`, `nav/`, `cards/`, `drawer/`, `settings/`, `channels/`, `processing/`, `auth/`,
|
||||
`operator/`, `join/`, `errors/`). Значения — 1:1 с текущими.
|
||||
- **T3. Миграция основного приложения** на `t()` (компоненты + вьюхи + store-слайсы).
|
||||
- **T4. Миграция оператор-консоли и страницы активации** (`operator/`, `join/`).
|
||||
- **T5. Локализация ошибок/статусов.** Маппинг известных `{detail}`/HTTP-кодов и статусов на ключи
|
||||
(`errors/*`); неизвестное — как есть.
|
||||
- **T6. Проверки.** Скрипт-«линтер»: нет кириллицы в шаблонах/логике вне словарей; `npm run build` зелёный.
|
||||
- **T7. Доки и STATUS.** Инструкция/техдок: устройство i18n и как добавить язык позже.
|
||||
|
||||
### Отложено (в бэклоге — делаем при появлении потребности)
|
||||
- Переключатель языка в UI и второй язык (en) — при потребности (ядро/`registerLocale` готовы).
|
||||
- Форматтеры Intl/плюрализация — вместе с языком.
|
||||
|
||||
## Границы
|
||||
|
||||
- Машинный автоперевод не делаем — словари добавляются вручную.
|
||||
- Локализация писем/внешних уведомлений — если появятся, отдельной задачей.
|
||||
# Дейл (Deal) — Этап 11: Локализация интерфейса (i18n)
|
||||
|
||||
> Исторический документ этапа 11. Актуальное состояние — `docs/superpowers/STATUS.md` и `docs/technical/Техническая-документация-Дейл.md`.
|
||||
|
||||
> Статус: план (не начат). Требование владельца от 2026-09-10.
|
||||
> Связанные документы: `docs/superpowers/plans/2026-09-05-deal-roadmap.md` (Этап 11),
|
||||
> `docs/spec/ТЗ-дейл-новая-архитектура.md` (§11, локализация), `docs/superpowers/STATUS.md` (Заделы).
|
||||
|
||||
## Цель
|
||||
|
||||
Весь интерфейс — на русском; **все** пользовательские тексты вынесены в ресурсы (словари), чтобы
|
||||
можно было добавлять новые языки и менять язык **на лету**. Русский — язык по умолчанию.
|
||||
|
||||
## Требования
|
||||
|
||||
- **Русский по умолчанию.** Все видимые строки UI: экраны, кнопки, подписи, заголовки, пустые состояния,
|
||||
подсказки, тултипы, тексты подтверждений, уведомления/тосты, страницы оператора и активации инвайта.
|
||||
- **Без хардкода.** Ни одна пользовательская строка не хранится в компонентах/шаблонах напрямую —
|
||||
только ключ в словаре. Технические строки (id/ключи/логи) не локализуются.
|
||||
- **Ошибки API.** Ответы бэка остаются `{detail}` + HTTP-код; фронт показывает локализованный текст по
|
||||
коду/ключу ошибки (расширяемый словарь ошибок). При необходимости бэк отдаёт код ошибки, а не только текст.
|
||||
- **Переключение на лету.** Смена языка без перезагрузки страницы; выбранный язык сохраняется
|
||||
(localStorage/настройки пользователя) и восстанавливается при входе.
|
||||
- **Расширяемость.** Новый язык = новый файл словаря (+ регистрация), без правок компонентов.
|
||||
- **Форматирование.** Даты/время/числа/валюты — через i18n-форматтеры; плюрализация — по правилам языка.
|
||||
Бэкенд-форматирование human-меток («только что», «N мин») — перевести на клиентские форматтеры или ключи.
|
||||
- **Ключи.** Стабильные, сгруппированные по областям (`nav/`, `cards/`, `settings/`, `operator/`, `errors/`…).
|
||||
Отсутствующий ключ в языке → фолбэк на русский (и, при необходимости, лог о пропуске).
|
||||
|
||||
## Область
|
||||
|
||||
- Основное приложение: дашборд, «Выбранные», настройки (все вкладки), каналы, обработка/состояние, вход.
|
||||
- Оператор-консоль (этап 10): все разделы + страница активации инвайта.
|
||||
|
||||
## Объём (по факту кода на 2026-09-10)
|
||||
|
||||
- 69 `.vue` + 20 `.js`; ~708 строковых литералов на кириллице в ~67 файлах
|
||||
(components ≈478, views ≈300, store ≈82) + текст прямо в шаблонах.
|
||||
- Области: навигация/шапка, карточки и колонки, драйвер карточки, настройки (все вкладки), каналы,
|
||||
обработка/состояние, вход, оператор-консоль (все разделы), страница активации, тосты/подтверждения.
|
||||
|
||||
## Решение владельца (2026-09-10)
|
||||
|
||||
- На этом этапе — **только русский**. Переключатель языка и второй язык — **в бэклоге**: делаем, когда
|
||||
возникнет потребность (см. «Отложено» ниже).
|
||||
- Задача этапа — **вынести все строки в ресурсы**, чтобы язык можно было добавить позже без правок компонентов.
|
||||
- Визуал и тексты — **1:1 с текущими** (вынос не меняет отображаемый текст).
|
||||
|
||||
## Задачи
|
||||
|
||||
- **T1. i18n-ядро (без тяжёлых зависимостей).** Composable/модуль: `t(key, params)`, реактивный `locale`
|
||||
(значение по умолчанию `ru`), `setLocale()` (архитектурно готов, UI-переключателя нет), загрузка
|
||||
словарей, фолбэк на ru при отсутствии ключа. `src/i18n/` + `locales/ru.js`.
|
||||
- **T2. Инвентаризация и словарь ru.** Вынести все строки в `locales/ru.js`, ключи сгруппированы по
|
||||
областям (`common/`, `nav/`, `cards/`, `drawer/`, `settings/`, `channels/`, `processing/`, `auth/`,
|
||||
`operator/`, `join/`, `errors/`). Значения — 1:1 с текущими.
|
||||
- **T3. Миграция основного приложения** на `t()` (компоненты + вьюхи + store-слайсы).
|
||||
- **T4. Миграция оператор-консоли и страницы активации** (`operator/`, `join/`).
|
||||
- **T5. Локализация ошибок/статусов.** Маппинг известных `{detail}`/HTTP-кодов и статусов на ключи
|
||||
(`errors/*`); неизвестное — как есть.
|
||||
- **T6. Проверки.** Скрипт-«линтер»: нет кириллицы в шаблонах/логике вне словарей; `npm run build` зелёный.
|
||||
- **T7. Доки и STATUS.** Инструкция/техдок: устройство i18n и как добавить язык позже.
|
||||
|
||||
### Отложено (в бэклоге — делаем при появлении потребности)
|
||||
- Переключатель языка в UI и второй язык (en) — при потребности (ядро/`registerLocale` готовы).
|
||||
- Форматтеры Intl/плюрализация — вместе с языком.
|
||||
|
||||
## Границы
|
||||
|
||||
- Машинный автоперевод не делаем — словари добавляются вручную.
|
||||
- Локализация писем/внешних уведомлений — если появятся, отдельной задачей.
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
# Дейл (Deal) — Этап 12: Наблюдаемость, устойчивость и производительность
|
||||
|
||||
> Исторический документ этапа 12. Актуальное состояние — `docs/superpowers/STATUS.md` и `docs/technical/Техническая-документация-Дейл.md`.
|
||||
|
||||
**Goal:** закрыть автономные заделы (без кредов и продуктовых решений): метрики Prometheus, распределённый
|
||||
rate-limit и инвалидация сессий, авто-очистки, перф фронта/бэка.
|
||||
|
||||
**Пакеты (порядок исполнения A → B → C → D).**
|
||||
|
||||
## Пакет A — Метрики (Prometheus + Grafana)
|
||||
|
||||
- Экспорт метрик по всем 4 процессам: HTTP/gRPC RPS, latency (p50/p95), ошибки 5xx, активные сессии,
|
||||
глубины очередей (pipeline, ML-outbox), счётчики токенов/аудита.
|
||||
- Общая обвязка для 3 сервисов — в `Deal.Grpc.Hosting`; core — в `Deal.Api`.
|
||||
- Эндпоинт `/metrics` (Prometheus-формат); сервис `prometheus` в профиле observability (`deploy/compose*.yml`),
|
||||
scrape-конфиг, Grafana-дашборды метрик + провайжининг datasource Prometheus.
|
||||
- Документация: как поднять профиль, где графики.
|
||||
|
||||
## Пакет B — Безопасность/устойчивость
|
||||
|
||||
- Распределённый rate-limit (хранилище на Postgres — без новой инфры) вместо in-memory; бэкенд учёта
|
||||
попыток входа (`LoginAttemptGuard`) на Postgres.
|
||||
- Мгновенный разлогин suspended-сессий: при suspend тенанта активные сессии перестают действовать (проверка
|
||||
статуса/инвалидация).
|
||||
- Авто-purge `audit_log` (retention, настройка/константа) и auto-purge истории `tenant_limits`.
|
||||
- Юнит-тесты + curl-приёмка в Docker.
|
||||
|
||||
## Пакет C — Производительность
|
||||
|
||||
- Фронт: вынести словарь i18n в ленивый чанк (устранить предупреждение >500 kB); пагинация/виртуализация
|
||||
длинных колонок.
|
||||
- telegram-service: LRU-кэши WTelegram (снижение памяти).
|
||||
- Механизм миграций на 1000 схем (производительность провижининга).
|
||||
- Линтер i18n включить в общий прогон `scripts/test.sh`.
|
||||
|
||||
## Пакет D — ИИ/ML без кредов
|
||||
|
||||
- `reclassify` на реальном ИИ: проводка + graceful-fallback/заглушка без кредов; тесты на Local-stub.
|
||||
- Расширение учёта токенов ML-пути (метрики/события).
|
||||
|
||||
## Границы
|
||||
|
||||
- Не входит (нужны креды/решения владельца): реальный Telegram-вход, живые LLM-вызовы, биллинг/планы,
|
||||
саморегистрация, Kafka, k8s/Cloudflare, ML export/import, переключатель языка/второй язык (в бэклоге — по потребности).
|
||||
- Каждый пакет: build 0/0, core-тесты, `npm run build`; при поднятии Docker — приёмка и **полная остановка**
|
||||
в конце (правило «без хвостов»).
|
||||
# Дейл (Deal) — Этап 12: Наблюдаемость, устойчивость и производительность
|
||||
|
||||
> Исторический документ этапа 12. Актуальное состояние — `docs/superpowers/STATUS.md` и `docs/technical/Техническая-документация-Дейл.md`.
|
||||
|
||||
**Goal:** закрыть автономные заделы (без кредов и продуктовых решений): метрики Prometheus, распределённый
|
||||
rate-limit и инвалидация сессий, авто-очистки, перф фронта/бэка.
|
||||
|
||||
**Пакеты (порядок исполнения A → B → C → D).**
|
||||
|
||||
## Пакет A — Метрики (Prometheus + Grafana)
|
||||
|
||||
- Экспорт метрик по всем 4 процессам: HTTP/gRPC RPS, latency (p50/p95), ошибки 5xx, активные сессии,
|
||||
глубины очередей (pipeline, ML-outbox), счётчики токенов/аудита.
|
||||
- Общая обвязка для 3 сервисов — в `Deal.Grpc.Hosting`; core — в `Deal.Api`.
|
||||
- Эндпоинт `/metrics` (Prometheus-формат); сервис `prometheus` в профиле observability (`deploy/compose*.yml`),
|
||||
scrape-конфиг, Grafana-дашборды метрик + провайжининг datasource Prometheus.
|
||||
- Документация: как поднять профиль, где графики.
|
||||
|
||||
## Пакет B — Безопасность/устойчивость
|
||||
|
||||
- Распределённый rate-limit (хранилище на Postgres — без новой инфры) вместо in-memory; бэкенд учёта
|
||||
попыток входа (`LoginAttemptGuard`) на Postgres.
|
||||
- Мгновенный разлогин suspended-сессий: при suspend тенанта активные сессии перестают действовать (проверка
|
||||
статуса/инвалидация).
|
||||
- Авто-purge `audit_log` (retention, настройка/константа) и auto-purge истории `tenant_limits`.
|
||||
- Юнит-тесты + curl-приёмка в Docker.
|
||||
|
||||
## Пакет C — Производительность
|
||||
|
||||
- Фронт: вынести словарь i18n в ленивый чанк (устранить предупреждение >500 kB); пагинация/виртуализация
|
||||
длинных колонок.
|
||||
- telegram-service: LRU-кэши WTelegram (снижение памяти).
|
||||
- Механизм миграций на 1000 схем (производительность провижининга).
|
||||
- Линтер i18n включить в общий прогон `scripts/test.sh`.
|
||||
|
||||
## Пакет D — ИИ/ML без кредов
|
||||
|
||||
- `reclassify` на реальном ИИ: проводка + graceful-fallback/заглушка без кредов; тесты на Local-stub.
|
||||
- Расширение учёта токенов ML-пути (метрики/события).
|
||||
|
||||
## Границы
|
||||
|
||||
- Не входит (нужны креды/решения владельца): реальный Telegram-вход, живые LLM-вызовы, биллинг/планы,
|
||||
саморегистрация, Kafka, k8s/Cloudflare, ML export/import, переключатель языка/второй язык (в бэклоге — по потребности).
|
||||
- Каждый пакет: build 0/0, core-тесты, `npm run build`; при поднятии Docker — приёмка и **полная остановка**
|
||||
в конце (правило «без хвостов»).
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# План: закрытие остатков код-стайла (2026-09-11, вечер)
|
||||
|
||||
> Источник: `backlog.md` — `TD-COMMENTS-IFACE` (п.3, п.4), `TD-STYLE-ANALYZERS`, найденное при проверке
|
||||
> проекта. Правила — `docs/spec/Код-стайл-Дейл.md`, отчёт — `docs/spec/Код-стайл-аудит-2026-09-11.md` §2.
|
||||
> Ограничения захода: без поднятия Docker-стека и без внешних кредов.
|
||||
|
||||
## Задачи
|
||||
|
||||
1. **Замер остатков** (dry-run, без правок): сканами по тексту и по имени члена проверить дубли
|
||||
`<summary>` реализации ↔ интерфейса; разбивку `var`; латинские комментарии; членов интерфейсов без
|
||||
дока; TODO; переводы строк по расширениям.
|
||||
2. **`var` для встроенных типов**: `.editorconfig` → `csharp_style_var_for_built_in_types = false:warning`
|
||||
(гейт ломает сборку), остаток выправить `dotnet format style --diagnostics IDE0008` по 5 решениям.
|
||||
«Очевидный тип» и «прочие» — оставить `silent` (субъективно, ~1600 замен).
|
||||
3. **Дедупликация `<summary>`→`<inheritdoc/>`**: по результатам замера — либо codemod, либо закрытие «дублей нет».
|
||||
4. **Переводы строк**: решение политики + нормализация (`.gitattributes`, `.editorconfig`, конверсия файлов,
|
||||
`git add --renormalize`); проверить, что `.sh` — LF (Linux CI).
|
||||
5. **Попутные доки/комментарии**: недостающие `<summary>` членам интерфейсов; англоязычные `//`-комментарии;
|
||||
повторный прогон `fix_private_docs.py`; устаревший блок в `STATUS.md`; трекаемые `.pyc` из индекса.
|
||||
6. **Приёмка**: build 5 sln 0/0, все тесты зелёные; обновить `backlog.md`/`STATUS.md`.
|
||||
|
||||
## Решения
|
||||
|
||||
- Явные реализации интерфейсов (§11) — **не автоматизировать**: остаётся точечным ревью владельца
|
||||
(замер: 54 интерфейса с XML-doc, 43 с реализациями; массовая правка ломает публичную поверхность классов).
|
||||
- Переводы строк — **LF** (инструменты проекта пишут LF; CRLF-.sh ломают `sh scripts/ci.sh` на Linux CI;
|
||||
большинство файлов уже LF). Откат — `git revert` нормализации.
|
||||
- Гейт `var` — только на встроенные типы: правило §4 запрет говорит про встроенные/неочевидные,
|
||||
«неочевидность» не проверяется машиной.
|
||||
|
||||
## Приёмка
|
||||
|
||||
- build 5 sln: 0 warnings / 0 errors (гейт IDE0008 проходит).
|
||||
- Тесты: core / telegram / ai / ml / storage — зелёные, счётчики в `STATUS.md`.
|
||||
- Фронт не менялся содержательно (только концы строк) — `build`/`lint:i18n` не прогонялись.
|
||||
@@ -1,228 +1,228 @@
|
||||
# Ревью качества кода «Дейл» (2026-09-08)
|
||||
|
||||
> Исторический документ этапа 8 (ревью, 2026-09-08). Актуальное состояние — `docs/superpowers/STATUS.md` и `docs/technical/Техническая-документация-Дейл.md`.
|
||||
|
||||
Многоосевое ревью (корректность/читаемость/архитектура/безопасность/производительность) бэкенда и
|
||||
фронтенда. Проводилось 5 ревьюерами по непересекающимся зонам (чтение; правок не вносилось), ключевые
|
||||
находки перепроверены по коду. Проект НЕ git. Метки: **[Critical]/[Required]/[Nit]/[Optional]**
|
||||
(Required = исправить до прода; Nit = желательно; Optional = задел).
|
||||
|
||||
## Сводка
|
||||
|
||||
| Зона | Объём | Critical | Required | Nit | Optional |
|
||||
|---|---|---|---|---|---|
|
||||
| Frontend (Vue3, JS) | 25 файлов / 11.3k LOC | 0 | 6 | 6 | 1 |
|
||||
| Core-каркас (Api/Infrastructure/Contracts) | ~370 файлов | 0 | 9 | 7 | 5 |
|
||||
| Модули Kanban/Pipeline/Projects | ~140 файлов | 0 | 10 | 5 | 2 |
|
||||
| Модули Settings/Telegram/Tenants/Discovery | ~130 файлов | 0 | 7 | 8 | 3 |
|
||||
| gRPC-сервисы (telegram/ai/ml) + proto | ~135 файлов | 0 | 8 | 8 | 4 |
|
||||
| **Итого** | **~1100 файлов** | **0** | **40** | **34** | **15** |
|
||||
|
||||
Общий вердикт: **код высокого качества** — чистая port&adapter-архитектура, 1 тип=1 файл, тенант-
|
||||
изоляция через схему на тенанта спроектирована сильно, SQL параметризован, XSS/секреты на фронте и в
|
||||
сервисах чистые. Найдено 0 критических дыр класса «ключ наружу/доступ к чужому тенанту». Ниже — что
|
||||
требует исправления и что стоит улучшить. Подробности по зонам — в рабочем журнале сессии (5 отчётов
|
||||
субагентов с file:line); здесь — консолидированный список.
|
||||
|
||||
---
|
||||
|
||||
## A. Безопасность (приоритет 1)
|
||||
|
||||
1. **[Required] SSRF через baseUrl ИИ-провайдера.** `Deal.Infrastructure/Integrations/AiConnectionChecker.cs`
|
||||
(проверка `ok:false/true`) + PATCH настроек разрешает тенанту задать произвольный `baseUrl` (в т.ч.
|
||||
`http://127.0.0.1:...` — подтверждено acceptance-логом task-6). На не-local провайдере ключ API уходит
|
||||
на указанный адрес → аутентифицированный тенант мультитенантного SaaS получает blind-сканер внутренней
|
||||
сети/метаданных. Исправить: резолв DNS + запрет private/link-local/loopback при проверке и вызове
|
||||
(или egress-фильтр); не принимать переопределение хоста для каталоговых провайдеров.
|
||||
2. **[Required] Rate-limit и анти-брутфорс выключены по умолчанию.** `Deal.Api/Program.cs` (регистрация
|
||||
лимитера), `RateLimitOptions` дефолт `Enabled=false` → без env в проде нет ни лимитов, ни
|
||||
`LoginAttemptGuard`. compose.prod форсирует `true`, но дефолт кода опасен при запуске вне compose.
|
||||
Исправить: стартовая проверка «Production ⇒ RateLimit:Enabled задан явно» (fail-closed).
|
||||
3. **[Required] CORS fail-open при пустом allowlist.** `Program.cs` (AddCors): пустой
|
||||
`Security:AllowedOrigins` = любой origin + `AllowCredentials` (задумано для dev). Исправить: в Production
|
||||
пустой список = отказ на старте; «any origin» только в Development.
|
||||
4. **[Required] Код инвайта пишется в audit_log сырым.** `JoinEndpoint.cs` — capability-токен в вечном
|
||||
аудите операторов. Исправить: не логировать код (или его SHA-256).
|
||||
5. **[Required] Пароль: минимум 4 символа.** `AuthEndpoints.cs`, `JoinEndpoint.cs`. Для публичного SaaS —
|
||||
минимум 8–10 + проверка на границе; единая константа.
|
||||
6. **[Required] Политика «ключ не перезаписывается маской» не реализована.** `SettingsService.cs`
|
||||
(aiConfigs и tgKeys): PATCH со значением-маской (например `sk-1…90ab`, ≥8 симв., без `enc:`) зашифрует
|
||||
маску и безвозвратно потеряет ключ. Комментарий «пустой/маска → не меняется» не подкреплён кодом.
|
||||
Исправить: не шифровать значение, содержащее `…` (U+2026) либо пустое; тест на roundtrip.
|
||||
7. **[Required] DDL прикладной ролью на старте и из tenant-ручки.** `TenantProvisioningService.cs`,
|
||||
`FtsMaintenance.cs` — `CREATE SCHEMA/Migrate/INDEX` на каждом старте и `/api/admin/fts/rebuild`.
|
||||
В проде это нарушение least privilege. Исправить: отдельные креды мигратора и runtime; fts-rebuild —
|
||||
операторской ручкой.
|
||||
8. **[Required] TenantId без инварианта формата.** `Deal.SharedKernel/Tenants/TenantId.cs` — значение идёт
|
||||
в Search Path строки подключения и в DDL; `new TenantId(внешняя_строка)` = connection-string-инъекция.
|
||||
Сейчас все потоки дают Guid, но тип не защищён. Исправить: конструктор от Guid / валидация 32 hex.
|
||||
9. **[Required] gRPC-сервисы: нет серверных лимитов на входные данные.** AiServiceImpl, MlServiceImpl,
|
||||
TelegramServiceImpl — контракты фиксируют лимиты («ядро обрежет»), но сервис их не enforcement:
|
||||
платные LLM-вызовы на мегабайтных промптах, гигантские SQLite-транзакции. Исправить:
|
||||
INVALID_ARGUMENT на границе + MaxReceiveMessageSize.
|
||||
10. **[Required] mTLS по умолчанию выключен — тихая деградация до plaintext.** `MtlsOptions.cs` —
|
||||
отсутствие/опечатка env молча даёт plaintext+только service-token. Исправить: fail-closed для
|
||||
Production (или warn-on-startup) как для session-ключа.
|
||||
11. **[Required] Инвайт: не проверяется существование/статус тенанта.** `JoinService.cs` — активация по
|
||||
«битому» инвайту даёт FK-500 или пользователя на несуществующем тенанте.
|
||||
12. **[Required] AddUsageAsync не атомарно.** `ITenantLimitStore.cs` — read-modify-write теряет списания
|
||||
при параллельных ИИ-вызовах. Исправить: `UPDATE ... SET Used=Used+@n`.
|
||||
13. **[Required] Echo-маска: секрет ≤8 символов отдаётся как есть.** `SettingsService.Mask` — маскировать
|
||||
всегда (кроме пустого).
|
||||
|
||||
## B. Корректность / потеря данных (приоритет 2)
|
||||
|
||||
14. **[Required] Потеря данных при параллельных мутациях JSON-массивов проектной карточки.**
|
||||
`ProjectsService.cs` (add_comment/add_link/remove_link), `ProjectFilesService.cs`: комментарии/ссылки/
|
||||
файлы дописываются «read → PATCH полной заменой массива» без версии/транзакции; double-click теряет
|
||||
запись. Исправить: append одним SQL (`jsonb ||`/`array_append`) или optimistic concurrency по `updated_at`.
|
||||
15. **[Required] Коллизия objectKey файла.** `ProjectFilesService.cs` — «проект/карточка/мс_имя»: две
|
||||
загрузки в одну мс = перезапись объекта. Исправить: случайный суффикс / id записи в ключе.
|
||||
16. **[Required] Дедуп-pump не атомарен.** `PipelineWorkerService.cs` — Exists→Claim→create без проверки
|
||||
результата claim — два конкурентных прохода создадут две карточки. Исправить: повторный Exists/
|
||||
проверка результата Claim перед созданием.
|
||||
17. **[Required] Move из trash/archive на доску минует снятие спам-сигнала.** `CardsService.cs` —
|
||||
валидируется только цель; «spam +1» не снимается (unlearn только в restore). Исправить: запрет исхода
|
||||
из archive/trash/taken в MoveLeadAsync (или симметричный unlearn).
|
||||
18. **[Required] Параллельные пустые `catch { }` в модулях Telegram/Discovery** — сбои зеркала/превью/
|
||||
backfill невидимы (ILogger в модулях не используется). Исправить: логировать.
|
||||
19. **[Required] ChangePassword (фронт) шлёт захардкоженный oldPassword='admin'.** `store.js`,
|
||||
`SettingsView.vue` — после смены пароля повторная смена невозможна, и пароль живёт в реактивном state.
|
||||
Исправить: поле «текущий пароль», не хранить пароль в store.
|
||||
20. **[Required] boot() роняет всё приложение одним сбоем** (фронт). `store.js`: параллельные get без
|
||||
.catch — падение /api/rates (например) = toast «Сервер недоступен» + разлогин. Исправить:
|
||||
необязательные секции в индивидуальные .catch; разлогин только при 401.
|
||||
21. **[Required] applySettings затирает несохранённые промпты** (фронт). `store.js` — автосейв тумблера
|
||||
применяет полный ответ и перезаписывает textarea промптов. Исправить: применять только запатченные ключи.
|
||||
22. **[Required] Гонки устаревших ответов поиска** (фронт). `store.js` — старый ответ может перетереть
|
||||
свежий/очищенный. Исправить: seq-токен/AbortController.
|
||||
23. **[Required] DeleteExpiredSessionsAsync на каждое разрешение сессии.** `AuthService.cs`,
|
||||
`OperatorAuthService.cs` — глобальный DELETE по public-таблицам в hot-path каждого запроса.
|
||||
Исправить: фоновый цикл или «с вероятностью N%»/логин.
|
||||
24. **[Required] ServiceTokenInterceptor проверяет токен только для unary RPC** — первый же
|
||||
server-streaming RPC пройдёт без проверки; то же в access-логе. Исправить: все 4 handler'а.
|
||||
25. **[Required] gRPC-логгер не логирует «прочие» исключения** (только OCE/RpcException) — 500-эквивалент
|
||||
уходит мимо лога. Исправить: catch (Exception) → log + RpcException.
|
||||
26. **[Required] Heartbeat/reconnect без таймаута** — зависший ConnectAsync последовательно блокирует
|
||||
все тенанты и shutdown. Исправить: CancelAfter на попытку.
|
||||
27. **[Required] QR: отмена RPC до первого URL не отменяет фоновую задачу** — «скрытая» авторизация.
|
||||
Исправить: отменять саму задачу при отмене ожидания.
|
||||
28. **[Required] TelegramBackfill fire-and-forget Task.Run из tenant-запроса без in-flight guard**
|
||||
(параллельные полные перечитывания); фоновые задачи не отслеживаются хостом. Исправить: гейт операции
|
||||
+ токен остановки хоста.
|
||||
29. **[Required] int.Parse(apiId)** из пользовательской KV-настройки `TelegramEndpoints.cs` —
|
||||
FormatException маскируется под 400 «не подключён». Исправить: TryParse + понятная ошибка.
|
||||
|
||||
## C. Архитектура / дублирование (приоритет 3)
|
||||
|
||||
30. **[Required]** 9 независимых реализаций чтения настроек (GetAsync+JsonDocument.Parse+дефолт) в
|
||||
Settings/IncomingRules/RatesService/Discovery*/DialogsService — расхождение семантики уже видно.
|
||||
**+** ~8 копий KV-хелперов (ReadBool/ReadInt/ReadString/ReadStringList) и 3 копии LoadRatesAsync в
|
||||
Kanban/Pipeline/Projects. Исправить: один публичный снапшот настроек в Settings или SharedKernel +
|
||||
общий RatesCacheReader.
|
||||
31. **[Required]** Обвязка gRPC-сервисов (ServiceTokenInterceptor/RpcCallLogging/MtlsOptions/MtlsCertificates/
|
||||
Logging + Host) скопирована в 3 независимых sln. Исправить: общий проект `Deal.Grpc.Hosting`.
|
||||
32. **[Required]** Большие файлы: PipelineWorkerService (914), KanbanStore (726), DiscoveryStore (632),
|
||||
ProjectsService (576), ProjectsEndpoints (568), CardsService (475), Program.cs (695), LocalFieldsParser
|
||||
(438), GrpcTelegramClient (447), TelegramIngressService (409); фронт: SettingsView.vue (1779),
|
||||
DiscoveryView.vue (1243), store.js (2434). Исправить: декомпозиция (см. ниже).
|
||||
33. **[Required] Фронт: MoveMenu вешает document-слушатель на каждую карточку** (сотни карточек → сотни
|
||||
слушателей). Исправить: один глобальный обработчик + id открытого меню в store.
|
||||
34. **[Required] Фронт: квадратичные пересчёты колонок.** `store.js` — filter+sort на каждую колонку/
|
||||
счётчик при каждом ре-рендере. Исправить: один computed Map<colId, sorted[]>.
|
||||
35. **[Nit]** Дублирование доменных констант между модулями (EmptyCommentDetail/JustNowLabel/MlSpamLabel/
|
||||
DefaultChannelHue/PlannedStage-литералы) и расхождение предиката «активные правила» (Kanban vs
|
||||
AiClassifyContextBuilder) — вынести в единые реестры.
|
||||
36. **[Nit]** Middleware сессий (Session vs OperatorSession) и токен-генераторы (SessionTokens/
|
||||
InviteCodeGenerator/TenantAdminService) дублируются — обобщить.
|
||||
37. **[Nit]** Легаси-ссылки на строки Python-прототипа в XML-doc (L177–191 и т.п.) — устаревают;
|
||||
оставить «зачем/инвариант», убрать номера строк.
|
||||
38. **[Nit]** Форматтеры времени и «знание» о контактах/типах файлов в 3–4 местах (фронт) — единый
|
||||
модуль форматов и словари меток.
|
||||
39. **[Nit]** `window.prompt` в renameBoard на фоне единого ConfirmDialog; дубликаты 86400000; ширины
|
||||
колонок sm/md/lg в 3 местах — константы/единый RenameDialog.
|
||||
|
||||
## D. Мёртвый код (кандидаты на удаление)
|
||||
|
||||
- Фронт: `utils.js` fileTypeInfo/EXT_KINDS/KIND_LABELS (не импортируется); `store.js` — curName/fmtMoney
|
||||
вне store, moveLead-мёртвая ветка, trashLead-пустой if, openDialog (не используется), checkReminders
|
||||
(нигде не вызывается); опция «mock»-курсов — проверить, жив ли режим на бэкенде.
|
||||
- Бэкенд: Kanban DemoLeadFactory недостижимый fallback PrimaryContact; DiscoverySearchErrorCounter —
|
||||
singleton-счётчик без TTL/эвикции и с межтенантным ключом (переделать per-tenant или чистить).
|
||||
|
||||
## E. Что соответствует хорошим практикам (подтверждено)
|
||||
|
||||
- Тенант-изоляция сильная: схема на тенанта через Search Path, TenantDbContext запрещён вне tenant-запроса
|
||||
(fail-fast), AsyncLocal сбрасывается в finally, gRPC-ингресс берёт tenant-id только из metadata, SSE
|
||||
per-tenant.
|
||||
- SQL параметризован везде (FromSqlInterpolated/ExecuteSqlInterpolated); массовые операции —
|
||||
ExecuteUpdate/Delete; комментарии-батчи без N+1; AsNoTracking.
|
||||
- Секреты не покидают систему: ключи шифруются (enc:+nonce‖ct‖tag), наружу маски; токены сессий — SHA-256
|
||||
хэши; пароли Argon2id; куки httpOnly+SameSite=Lax; fail-closed service-token (с явным гардом
|
||||
«пусто≠пусто»); path traversal защищён (SessionStore/ModelPool валидируют tenant-id как имя файла).
|
||||
- Фронт: XSS-аудит чистый (v-html только через экранирующий renderSourceMessage со схемами http/tg),
|
||||
токенов в localStorage нет (httpOnly-кука), все target=_blank с rel=noreferrer.
|
||||
- Чистая архитектура port&adapter в модулях (нет EF/HTTP в Application), DTO-рекорды, DI-Registrar'ы,
|
||||
направленные зависимости без циклов, константы-каталоги вместо магических строк.
|
||||
|
||||
## F. Рекомендуемый порядок исправлений
|
||||
|
||||
1. **Безопасность (A1–A13)** — до любого прода. Точечные правки + тесты.
|
||||
2. **Потеря данных/корректность (B14–B29)** — гонки, дедуп, маски, boot/applySettings фронта.
|
||||
3. **Архитектура (C30–C34)** — вынос общего grpc-hosting, снапшот настроек, декомпозиция больших файлов,
|
||||
фронт: leadsByCol-компьютед и глобальный слушатель меню.
|
||||
4. **Чистка мёртвого кода (D)** + реестры констант (C35–C39) — в рамках рефакторингов, не отдельно.
|
||||
5. **Заделы (Optional)** — пагинация колонок, виртуализация списков, LRU для кэшей сессий WTelegram,
|
||||
батчинг провижининга схем, MinIO tenant-префикс, per-request size-лимиты загрузок, single-flight
|
||||
DiscoveryWorker.
|
||||
|
||||
---
|
||||
|
||||
## Статус исправлений (2026-09-08, после ревью)
|
||||
|
||||
Выполнено в ходе rework-захода (детали — `.superpowers/sdd/deal-stage8-quality-rework/progress.md` и
|
||||
`docs/superpowers/STATUS.md`). Тесты: core **1135/1135**, telegram **118/118**, ai **52/52**, ml **38/38**,
|
||||
фронт `npm run build` OK.
|
||||
|
||||
**A. Безопасность — закрыто (A1–A13):**
|
||||
- A1 SSRF: `SettingsService` — baseUrl каталоговых облачных провайдеров не переопределяется (только
|
||||
local/custom); `AiConnectionChecker` — запрет private/loopback/link-local адресов (в т.ч. 169.254.169.254).
|
||||
- A2/A3: fail-closed в Production (RateLimit:Enabled обязателен, CORS-allowlist непустой, conn-string без
|
||||
фолбэка) — стартовые проверки `Program.cs`.
|
||||
- A4: код инвайта в аудите → SHA-256 `codeHash` (3 события, тесты обновлены).
|
||||
- A5: пароль минимум 8 (единый `AuthService.MinNewPasswordLength`).
|
||||
- A6: PATCH с маской ключа («…») больше не шифрует маску (терялся бы ключ); A13: короткие секреты
|
||||
маскируются всегда (`MaskSecret`), apiId остаётся как есть (не секрет).
|
||||
- A7: DDL (провижининг схем/миграции) — опциональная мигратор-строка `ConnectionStrings:DealMigrator`
|
||||
(`ConnectionStringProvider.ForSchemaDdl`); dev/тесты — прежнее поведение.
|
||||
- A8: `TenantId` — инвариант 32 hex (Guid N), фабрика FromGuid.
|
||||
- A9: gRPC-сервисы — лимиты входных данных (INVALID_ARGUMENT) + MaxReceiveMessageSize=4MiB.
|
||||
- A10: mTLS fail-closed в Production (сервисы).
|
||||
- A11: `JoinService` — целевой тенант обязан существовать и быть активным до резервирования кода.
|
||||
- A12: атомарный инкремент токенов (`UPDATE ... UsedTokens=UsedTokens+@n`) для Npgsql; EF-путь для InMemory.
|
||||
- Доп.: int.TryParse apiId; ResolveSession учитывает статус пользователя; очистка протухших сессий — вне
|
||||
hot-path.
|
||||
|
||||
**B. Корректность/потеря данных — закрыто (B14–B29):** атомарные append (comment/link/file) в ProjectStore
|
||||
(1 SQL), objectKey файла с id записи, дедуп-pump атомарен (Claim→bool), move из trash/archive/taken запрещён,
|
||||
пустые catch логируются (DiscLog/ILogger), фронт: смена пароля (oldPass), boot с .catch, applySettings не
|
||||
затирает промпты, seq-токены поиска; интерцепторы gRPC на все 4 вида RPC, логгер catch(Exception), reconnect
|
||||
с таймаутом, QR-cancel; backfill с in-flight guard + lifetime-токеном.
|
||||
|
||||
**C. Архитектура — закрыто:** C30 (единый `TenantSettingsSnapshot` вместо ~9 копий чтения настроек и 3 копий
|
||||
`LoadRatesAsync`; удалён клон `RateTable.cs`), C31 (общий `src/grpc-hosting/Deal.Grpc.Hosting`; 15 файлов
|
||||
дублей удалены), C32-декомпозиция (KanbanStore→5, PipelineWorkerService→8, DiscoveryStore→5, ProjectsService→4,
|
||||
CardsService→3, SettingsService→6, DiscoveryWorkerService→6 partial; фронт: store.js→слайсы store/, вынесены
|
||||
Telegram/Stop/Scope-вкладки SettingsView, DiscoveryCandidateCard), C33/C34 (MoveMenu, leadsByCol), C36
|
||||
(`UrlSafeToken`). **Закрыто после ревью (2026-09-09):** C35 — общие реестры
|
||||
`MlLearningLabels`/`SourceDefaults` в Deal.Contracts (метки обучения ML «spam»/«t:hire»/«t:order» и дефолтный
|
||||
цвет источника «#666») вместо дублей MlSpamLabel/DefaultChannelHue/DefaultDialogHue/SpamLabel в
|
||||
Pipeline/Discovery/Telegram/Infrastructure; единый предикат «активные правила» — AiClassifyContextBuilder
|
||||
переведён на `ColumnRules.HasActiveRules` (Kanban; было расхождение Count>0 vs терм после trim); реестр
|
||||
`ProjectStages` (9 id-констант вместо литералов) и общий `CardsService.JustNowLabel` (Projects/адаптер
|
||||
KanbanStore); DiscoverySearchErrorCounter — TTL-эвикция (см. D). **Задел:** полный вынос остальных вкладок
|
||||
SettingsView (риск без e2e).
|
||||
|
||||
**D. Мёртвый код:** удалён (фронт: fileTypeInfo/EXT_KINDS/curName/fmtMoney/openDialog/checkReminders и др.;
|
||||
бэкенд: недостижимый PrimaryContact DemoLeadFactory и др.). DiscoverySearchErrorCounter — добавлена TTL-эвикция
|
||||
записей (EntryTtlSeconds=1 ч, ленивая при Next/Reset, часы инъекцией; +4 теста) — задел D закрыт.
|
||||
# Ревью качества кода «Дейл» (2026-09-08)
|
||||
|
||||
> Исторический документ этапа 8 (ревью, 2026-09-08). Актуальное состояние — `docs/superpowers/STATUS.md` и `docs/technical/Техническая-документация-Дейл.md`.
|
||||
|
||||
Многоосевое ревью (корректность/читаемость/архитектура/безопасность/производительность) бэкенда и
|
||||
фронтенда. Проводилось 5 ревьюерами по непересекающимся зонам (чтение; правок не вносилось), ключевые
|
||||
находки перепроверены по коду. Проект НЕ git. Метки: **[Critical]/[Required]/[Nit]/[Optional]**
|
||||
(Required = исправить до прода; Nit = желательно; Optional = задел).
|
||||
|
||||
## Сводка
|
||||
|
||||
| Зона | Объём | Critical | Required | Nit | Optional |
|
||||
|---|---|---|---|---|---|
|
||||
| Frontend (Vue3, JS) | 25 файлов / 11.3k LOC | 0 | 6 | 6 | 1 |
|
||||
| Core-каркас (Api/Infrastructure/Contracts) | ~370 файлов | 0 | 9 | 7 | 5 |
|
||||
| Модули Kanban/Pipeline/Projects | ~140 файлов | 0 | 10 | 5 | 2 |
|
||||
| Модули Settings/Telegram/Tenants/Discovery | ~130 файлов | 0 | 7 | 8 | 3 |
|
||||
| gRPC-сервисы (telegram/ai/ml) + proto | ~135 файлов | 0 | 8 | 8 | 4 |
|
||||
| **Итого** | **~1100 файлов** | **0** | **40** | **34** | **15** |
|
||||
|
||||
Общий вердикт: **код высокого качества** — чистая port&adapter-архитектура, 1 тип=1 файл, тенант-
|
||||
изоляция через схему на тенанта спроектирована сильно, SQL параметризован, XSS/секреты на фронте и в
|
||||
сервисах чистые. Найдено 0 критических дыр класса «ключ наружу/доступ к чужому тенанту». Ниже — что
|
||||
требует исправления и что стоит улучшить. Подробности по зонам — в рабочем журнале сессии (5 отчётов
|
||||
субагентов с file:line); здесь — консолидированный список.
|
||||
|
||||
---
|
||||
|
||||
## A. Безопасность (приоритет 1)
|
||||
|
||||
1. **[Required] SSRF через baseUrl ИИ-провайдера.** `Deal.Infrastructure/Integrations/AiConnectionChecker.cs`
|
||||
(проверка `ok:false/true`) + PATCH настроек разрешает тенанту задать произвольный `baseUrl` (в т.ч.
|
||||
`http://127.0.0.1:...` — подтверждено acceptance-логом task-6). На не-local провайдере ключ API уходит
|
||||
на указанный адрес → аутентифицированный тенант мультитенантного SaaS получает blind-сканер внутренней
|
||||
сети/метаданных. Исправить: резолв DNS + запрет private/link-local/loopback при проверке и вызове
|
||||
(или egress-фильтр); не принимать переопределение хоста для каталоговых провайдеров.
|
||||
2. **[Required] Rate-limit и анти-брутфорс выключены по умолчанию.** `Deal.Api/Program.cs` (регистрация
|
||||
лимитера), `RateLimitOptions` дефолт `Enabled=false` → без env в проде нет ни лимитов, ни
|
||||
`LoginAttemptGuard`. compose.prod форсирует `true`, но дефолт кода опасен при запуске вне compose.
|
||||
Исправить: стартовая проверка «Production ⇒ RateLimit:Enabled задан явно» (fail-closed).
|
||||
3. **[Required] CORS fail-open при пустом allowlist.** `Program.cs` (AddCors): пустой
|
||||
`Security:AllowedOrigins` = любой origin + `AllowCredentials` (задумано для dev). Исправить: в Production
|
||||
пустой список = отказ на старте; «any origin» только в Development.
|
||||
4. **[Required] Код инвайта пишется в audit_log сырым.** `JoinEndpoint.cs` — capability-токен в вечном
|
||||
аудите операторов. Исправить: не логировать код (или его SHA-256).
|
||||
5. **[Required] Пароль: минимум 4 символа.** `AuthEndpoints.cs`, `JoinEndpoint.cs`. Для публичного SaaS —
|
||||
минимум 8–10 + проверка на границе; единая константа.
|
||||
6. **[Required] Политика «ключ не перезаписывается маской» не реализована.** `SettingsService.cs`
|
||||
(aiConfigs и tgKeys): PATCH со значением-маской (например `sk-1…90ab`, ≥8 симв., без `enc:`) зашифрует
|
||||
маску и безвозвратно потеряет ключ. Комментарий «пустой/маска → не меняется» не подкреплён кодом.
|
||||
Исправить: не шифровать значение, содержащее `…` (U+2026) либо пустое; тест на roundtrip.
|
||||
7. **[Required] DDL прикладной ролью на старте и из tenant-ручки.** `TenantProvisioningService.cs`,
|
||||
`FtsMaintenance.cs` — `CREATE SCHEMA/Migrate/INDEX` на каждом старте и `/api/admin/fts/rebuild`.
|
||||
В проде это нарушение least privilege. Исправить: отдельные креды мигратора и runtime; fts-rebuild —
|
||||
операторской ручкой.
|
||||
8. **[Required] TenantId без инварианта формата.** `Deal.SharedKernel/Tenants/TenantId.cs` — значение идёт
|
||||
в Search Path строки подключения и в DDL; `new TenantId(внешняя_строка)` = connection-string-инъекция.
|
||||
Сейчас все потоки дают Guid, но тип не защищён. Исправить: конструктор от Guid / валидация 32 hex.
|
||||
9. **[Required] gRPC-сервисы: нет серверных лимитов на входные данные.** AiServiceImpl, MlServiceImpl,
|
||||
TelegramServiceImpl — контракты фиксируют лимиты («ядро обрежет»), но сервис их не enforcement:
|
||||
платные LLM-вызовы на мегабайтных промптах, гигантские SQLite-транзакции. Исправить:
|
||||
INVALID_ARGUMENT на границе + MaxReceiveMessageSize.
|
||||
10. **[Required] mTLS по умолчанию выключен — тихая деградация до plaintext.** `MtlsOptions.cs` —
|
||||
отсутствие/опечатка env молча даёт plaintext+только service-token. Исправить: fail-closed для
|
||||
Production (или warn-on-startup) как для session-ключа.
|
||||
11. **[Required] Инвайт: не проверяется существование/статус тенанта.** `JoinService.cs` — активация по
|
||||
«битому» инвайту даёт FK-500 или пользователя на несуществующем тенанте.
|
||||
12. **[Required] AddUsageAsync не атомарно.** `ITenantLimitStore.cs` — read-modify-write теряет списания
|
||||
при параллельных ИИ-вызовах. Исправить: `UPDATE ... SET Used=Used+@n`.
|
||||
13. **[Required] Echo-маска: секрет ≤8 символов отдаётся как есть.** `SettingsService.Mask` — маскировать
|
||||
всегда (кроме пустого).
|
||||
|
||||
## B. Корректность / потеря данных (приоритет 2)
|
||||
|
||||
14. **[Required] Потеря данных при параллельных мутациях JSON-массивов проектной карточки.**
|
||||
`ProjectsService.cs` (add_comment/add_link/remove_link), `ProjectFilesService.cs`: комментарии/ссылки/
|
||||
файлы дописываются «read → PATCH полной заменой массива» без версии/транзакции; double-click теряет
|
||||
запись. Исправить: append одним SQL (`jsonb ||`/`array_append`) или optimistic concurrency по `updated_at`.
|
||||
15. **[Required] Коллизия objectKey файла.** `ProjectFilesService.cs` — «проект/карточка/мс_имя»: две
|
||||
загрузки в одну мс = перезапись объекта. Исправить: случайный суффикс / id записи в ключе.
|
||||
16. **[Required] Дедуп-pump не атомарен.** `PipelineWorkerService.cs` — Exists→Claim→create без проверки
|
||||
результата claim — два конкурентных прохода создадут две карточки. Исправить: повторный Exists/
|
||||
проверка результата Claim перед созданием.
|
||||
17. **[Required] Move из trash/archive на доску минует снятие спам-сигнала.** `CardsService.cs` —
|
||||
валидируется только цель; «spam +1» не снимается (unlearn только в restore). Исправить: запрет исхода
|
||||
из archive/trash/taken в MoveLeadAsync (или симметричный unlearn).
|
||||
18. **[Required] Параллельные пустые `catch { }` в модулях Telegram/Discovery** — сбои зеркала/превью/
|
||||
backfill невидимы (ILogger в модулях не используется). Исправить: логировать.
|
||||
19. **[Required] ChangePassword (фронт) шлёт захардкоженный oldPassword='admin'.** `store.js`,
|
||||
`SettingsView.vue` — после смены пароля повторная смена невозможна, и пароль живёт в реактивном state.
|
||||
Исправить: поле «текущий пароль», не хранить пароль в store.
|
||||
20. **[Required] boot() роняет всё приложение одним сбоем** (фронт). `store.js`: параллельные get без
|
||||
.catch — падение /api/rates (например) = toast «Сервер недоступен» + разлогин. Исправить:
|
||||
необязательные секции в индивидуальные .catch; разлогин только при 401.
|
||||
21. **[Required] applySettings затирает несохранённые промпты** (фронт). `store.js` — автосейв тумблера
|
||||
применяет полный ответ и перезаписывает textarea промптов. Исправить: применять только запатченные ключи.
|
||||
22. **[Required] Гонки устаревших ответов поиска** (фронт). `store.js` — старый ответ может перетереть
|
||||
свежий/очищенный. Исправить: seq-токен/AbortController.
|
||||
23. **[Required] DeleteExpiredSessionsAsync на каждое разрешение сессии.** `AuthService.cs`,
|
||||
`OperatorAuthService.cs` — глобальный DELETE по public-таблицам в hot-path каждого запроса.
|
||||
Исправить: фоновый цикл или «с вероятностью N%»/логин.
|
||||
24. **[Required] ServiceTokenInterceptor проверяет токен только для unary RPC** — первый же
|
||||
server-streaming RPC пройдёт без проверки; то же в access-логе. Исправить: все 4 handler'а.
|
||||
25. **[Required] gRPC-логгер не логирует «прочие» исключения** (только OCE/RpcException) — 500-эквивалент
|
||||
уходит мимо лога. Исправить: catch (Exception) → log + RpcException.
|
||||
26. **[Required] Heartbeat/reconnect без таймаута** — зависший ConnectAsync последовательно блокирует
|
||||
все тенанты и shutdown. Исправить: CancelAfter на попытку.
|
||||
27. **[Required] QR: отмена RPC до первого URL не отменяет фоновую задачу** — «скрытая» авторизация.
|
||||
Исправить: отменять саму задачу при отмене ожидания.
|
||||
28. **[Required] TelegramBackfill fire-and-forget Task.Run из tenant-запроса без in-flight guard**
|
||||
(параллельные полные перечитывания); фоновые задачи не отслеживаются хостом. Исправить: гейт операции
|
||||
+ токен остановки хоста.
|
||||
29. **[Required] int.Parse(apiId)** из пользовательской KV-настройки `TelegramEndpoints.cs` —
|
||||
FormatException маскируется под 400 «не подключён». Исправить: TryParse + понятная ошибка.
|
||||
|
||||
## C. Архитектура / дублирование (приоритет 3)
|
||||
|
||||
30. **[Required]** 9 независимых реализаций чтения настроек (GetAsync+JsonDocument.Parse+дефолт) в
|
||||
Settings/IncomingRules/RatesService/Discovery*/DialogsService — расхождение семантики уже видно.
|
||||
**+** ~8 копий KV-хелперов (ReadBool/ReadInt/ReadString/ReadStringList) и 3 копии LoadRatesAsync в
|
||||
Kanban/Pipeline/Projects. Исправить: один публичный снапшот настроек в Settings или SharedKernel +
|
||||
общий RatesCacheReader.
|
||||
31. **[Required]** Обвязка gRPC-сервисов (ServiceTokenInterceptor/RpcCallLogging/MtlsOptions/MtlsCertificates/
|
||||
Logging + Host) скопирована в 3 независимых sln. Исправить: общий проект `Deal.Grpc.Hosting`.
|
||||
32. **[Required]** Большие файлы: PipelineWorkerService (914), KanbanStore (726), DiscoveryStore (632),
|
||||
ProjectsService (576), ProjectsEndpoints (568), CardsService (475), Program.cs (695), LocalFieldsParser
|
||||
(438), GrpcTelegramClient (447), TelegramIngressService (409); фронт: SettingsView.vue (1779),
|
||||
DiscoveryView.vue (1243), store.js (2434). Исправить: декомпозиция (см. ниже).
|
||||
33. **[Required] Фронт: MoveMenu вешает document-слушатель на каждую карточку** (сотни карточек → сотни
|
||||
слушателей). Исправить: один глобальный обработчик + id открытого меню в store.
|
||||
34. **[Required] Фронт: квадратичные пересчёты колонок.** `store.js` — filter+sort на каждую колонку/
|
||||
счётчик при каждом ре-рендере. Исправить: один computed Map<colId, sorted[]>.
|
||||
35. **[Nit]** Дублирование доменных констант между модулями (EmptyCommentDetail/JustNowLabel/MlSpamLabel/
|
||||
DefaultChannelHue/PlannedStage-литералы) и расхождение предиката «активные правила» (Kanban vs
|
||||
AiClassifyContextBuilder) — вынести в единые реестры.
|
||||
36. **[Nit]** Middleware сессий (Session vs OperatorSession) и токен-генераторы (SessionTokens/
|
||||
InviteCodeGenerator/TenantAdminService) дублируются — обобщить.
|
||||
37. **[Nit]** Легаси-ссылки на строки Python-прототипа в XML-doc (L177–191 и т.п.) — устаревают;
|
||||
оставить «зачем/инвариант», убрать номера строк.
|
||||
38. **[Nit]** Форматтеры времени и «знание» о контактах/типах файлов в 3–4 местах (фронт) — единый
|
||||
модуль форматов и словари меток.
|
||||
39. **[Nit]** `window.prompt` в renameBoard на фоне единого ConfirmDialog; дубликаты 86400000; ширины
|
||||
колонок sm/md/lg в 3 местах — константы/единый RenameDialog.
|
||||
|
||||
## D. Мёртвый код (кандидаты на удаление)
|
||||
|
||||
- Фронт: `utils.js` fileTypeInfo/EXT_KINDS/KIND_LABELS (не импортируется); `store.js` — curName/fmtMoney
|
||||
вне store, moveLead-мёртвая ветка, trashLead-пустой if, openDialog (не используется), checkReminders
|
||||
(нигде не вызывается); опция «mock»-курсов — проверить, жив ли режим на бэкенде.
|
||||
- Бэкенд: Kanban DemoLeadFactory недостижимый fallback PrimaryContact; DiscoverySearchErrorCounter —
|
||||
singleton-счётчик без TTL/эвикции и с межтенантным ключом (переделать per-tenant или чистить).
|
||||
|
||||
## E. Что соответствует хорошим практикам (подтверждено)
|
||||
|
||||
- Тенант-изоляция сильная: схема на тенанта через Search Path, TenantDbContext запрещён вне tenant-запроса
|
||||
(fail-fast), AsyncLocal сбрасывается в finally, gRPC-ингресс берёт tenant-id только из metadata, SSE
|
||||
per-tenant.
|
||||
- SQL параметризован везде (FromSqlInterpolated/ExecuteSqlInterpolated); массовые операции —
|
||||
ExecuteUpdate/Delete; комментарии-батчи без N+1; AsNoTracking.
|
||||
- Секреты не покидают систему: ключи шифруются (enc:+nonce‖ct‖tag), наружу маски; токены сессий — SHA-256
|
||||
хэши; пароли Argon2id; куки httpOnly+SameSite=Lax; fail-closed service-token (с явным гардом
|
||||
«пусто≠пусто»); path traversal защищён (SessionStore/ModelPool валидируют tenant-id как имя файла).
|
||||
- Фронт: XSS-аудит чистый (v-html только через экранирующий renderSourceMessage со схемами http/tg),
|
||||
токенов в localStorage нет (httpOnly-кука), все target=_blank с rel=noreferrer.
|
||||
- Чистая архитектура port&adapter в модулях (нет EF/HTTP в Application), DTO-рекорды, DI-Registrar'ы,
|
||||
направленные зависимости без циклов, константы-каталоги вместо магических строк.
|
||||
|
||||
## F. Рекомендуемый порядок исправлений
|
||||
|
||||
1. **Безопасность (A1–A13)** — до любого прода. Точечные правки + тесты.
|
||||
2. **Потеря данных/корректность (B14–B29)** — гонки, дедуп, маски, boot/applySettings фронта.
|
||||
3. **Архитектура (C30–C34)** — вынос общего grpc-hosting, снапшот настроек, декомпозиция больших файлов,
|
||||
фронт: leadsByCol-компьютед и глобальный слушатель меню.
|
||||
4. **Чистка мёртвого кода (D)** + реестры констант (C35–C39) — в рамках рефакторингов, не отдельно.
|
||||
5. **Заделы (Optional)** — пагинация колонок, виртуализация списков, LRU для кэшей сессий WTelegram,
|
||||
батчинг провижининга схем, MinIO tenant-префикс, per-request size-лимиты загрузок, single-flight
|
||||
DiscoveryWorker.
|
||||
|
||||
---
|
||||
|
||||
## Статус исправлений (2026-09-08, после ревью)
|
||||
|
||||
Выполнено в ходе rework-захода (детали — `.superpowers/sdd/deal-stage8-quality-rework/progress.md` и
|
||||
`docs/superpowers/STATUS.md`). Тесты: core **1135/1135**, telegram **118/118**, ai **52/52**, ml **38/38**,
|
||||
фронт `npm run build` OK.
|
||||
|
||||
**A. Безопасность — закрыто (A1–A13):**
|
||||
- A1 SSRF: `SettingsService` — baseUrl каталоговых облачных провайдеров не переопределяется (только
|
||||
local/custom); `AiConnectionChecker` — запрет private/loopback/link-local адресов (в т.ч. 169.254.169.254).
|
||||
- A2/A3: fail-closed в Production (RateLimit:Enabled обязателен, CORS-allowlist непустой, conn-string без
|
||||
фолбэка) — стартовые проверки `Program.cs`.
|
||||
- A4: код инвайта в аудите → SHA-256 `codeHash` (3 события, тесты обновлены).
|
||||
- A5: пароль минимум 8 (единый `AuthService.MinNewPasswordLength`).
|
||||
- A6: PATCH с маской ключа («…») больше не шифрует маску (терялся бы ключ); A13: короткие секреты
|
||||
маскируются всегда (`MaskSecret`), apiId остаётся как есть (не секрет).
|
||||
- A7: DDL (провижининг схем/миграции) — опциональная мигратор-строка `ConnectionStrings:DealMigrator`
|
||||
(`ConnectionStringProvider.ForSchemaDdl`); dev/тесты — прежнее поведение.
|
||||
- A8: `TenantId` — инвариант 32 hex (Guid N), фабрика FromGuid.
|
||||
- A9: gRPC-сервисы — лимиты входных данных (INVALID_ARGUMENT) + MaxReceiveMessageSize=4MiB.
|
||||
- A10: mTLS fail-closed в Production (сервисы).
|
||||
- A11: `JoinService` — целевой тенант обязан существовать и быть активным до резервирования кода.
|
||||
- A12: атомарный инкремент токенов (`UPDATE ... UsedTokens=UsedTokens+@n`) для Npgsql; EF-путь для InMemory.
|
||||
- Доп.: int.TryParse apiId; ResolveSession учитывает статус пользователя; очистка протухших сессий — вне
|
||||
hot-path.
|
||||
|
||||
**B. Корректность/потеря данных — закрыто (B14–B29):** атомарные append (comment/link/file) в ProjectStore
|
||||
(1 SQL), objectKey файла с id записи, дедуп-pump атомарен (Claim→bool), move из trash/archive/taken запрещён,
|
||||
пустые catch логируются (DiscLog/ILogger), фронт: смена пароля (oldPass), boot с .catch, applySettings не
|
||||
затирает промпты, seq-токены поиска; интерцепторы gRPC на все 4 вида RPC, логгер catch(Exception), reconnect
|
||||
с таймаутом, QR-cancel; backfill с in-flight guard + lifetime-токеном.
|
||||
|
||||
**C. Архитектура — закрыто:** C30 (единый `TenantSettingsSnapshot` вместо ~9 копий чтения настроек и 3 копий
|
||||
`LoadRatesAsync`; удалён клон `RateTable.cs`), C31 (общий `src/grpc-hosting/Deal.Grpc.Hosting`; 15 файлов
|
||||
дублей удалены), C32-декомпозиция (KanbanStore→5, PipelineWorkerService→8, DiscoveryStore→5, ProjectsService→4,
|
||||
CardsService→3, SettingsService→6, DiscoveryWorkerService→6 partial; фронт: store.js→слайсы store/, вынесены
|
||||
Telegram/Stop/Scope-вкладки SettingsView, DiscoveryCandidateCard), C33/C34 (MoveMenu, leadsByCol), C36
|
||||
(`UrlSafeToken`). **Закрыто после ревью (2026-09-09):** C35 — общие реестры
|
||||
`MlLearningLabels`/`SourceDefaults` в Deal.Contracts (метки обучения ML «spam»/«t:hire»/«t:order» и дефолтный
|
||||
цвет источника «#666») вместо дублей MlSpamLabel/DefaultChannelHue/DefaultDialogHue/SpamLabel в
|
||||
Pipeline/Discovery/Telegram/Infrastructure; единый предикат «активные правила» — AiClassifyContextBuilder
|
||||
переведён на `ColumnRules.HasActiveRules` (Kanban; было расхождение Count>0 vs терм после trim); реестр
|
||||
`ProjectStages` (9 id-констант вместо литералов) и общий `CardsService.JustNowLabel` (Projects/адаптер
|
||||
KanbanStore); DiscoverySearchErrorCounter — TTL-эвикция (см. D). **Задел:** полный вынос остальных вкладок
|
||||
SettingsView (риск без e2e).
|
||||
|
||||
**D. Мёртвый код:** удалён (фронт: fileTypeInfo/EXT_KINDS/curName/fmtMoney/openDialog/checkReminders и др.;
|
||||
бэкенд: недостижимый PrimaryContact DemoLeadFactory и др.). DiscoverySearchErrorCounter — добавлена TTL-эвикция
|
||||
записей (EntryTtlSeconds=1 ч, ленивая при Next/Reset, часы инъекцией; +4 теста) — задел D закрыт.
|
||||
|
||||
@@ -1,113 +1,113 @@
|
||||
# Аудит документации «Дейл»: сверка с кодом/конфигами
|
||||
|
||||
> Исторический документ (аудит документации, 2026-09-10; следующий — `2026-09-11-docs-final-sweep.md`). Актуальное состояние — `docs/superpowers/STATUS.md` и `docs/technical/Техническая-документация-Дейл.md`.
|
||||
|
||||
> Дата: 2026-09-11
|
||||
> Проверено: `docs/spec/ТЗ-дейл-новая-архитектура.md`,
|
||||
> `docs/user-guide/Инструкция-пользователя-Дейл.md`,
|
||||
> `docs/technical/Техническая-документация-Дейл.md`,
|
||||
> `docs/api/api-map.md`, плюс `docs/superpowers/STATUS.md`.
|
||||
> Метод: сверка утверждений с кодом (`src/core/Deal.Api/Endpoints/*`,
|
||||
> `src/core/Deal.Infrastructure/**`, `src/frontend/src/**`, `src/{ai,ml,telegram}-service`),
|
||||
> конфигами (`deploy/compose.*.yml`, `appsettings*.json`) и скриптами (`scripts/*.sh`).
|
||||
> Докер не поднимался, тесты не перезапускались (см. «непроверяемое»).
|
||||
|
||||
## Сводка
|
||||
|
||||
- Найдено расхождений: **30** (по пунктам таблиц ниже).
|
||||
- Исправлено прямо в доках: **30**.
|
||||
- Значимые подтверждённые факты, с которыми доки сходятся: порты (core 5080/5082, telegram 5101,
|
||||
ai 5102, ml 5103, metrics 9464, postgres 5433, minio 9000/9001, grafana 3001), единые домены
|
||||
`/api/cards` + `/api/containers`, оператор-консоль `#/operator` и активация `#/join`,
|
||||
ключи Telegram — у оператора (`global_settings`), команды запуска.
|
||||
|
||||
## Расхождения (файл:строка → в доке → реальность → исправлено)
|
||||
|
||||
### `docs/technical/Техническая-документация-Дейл.md`
|
||||
|
||||
| # | Место | В доке | Реальность (код) | Статус |
|
||||
|---|---|---|---|---|
|
||||
| 1 | §2 «Структура» (~L43) | проект `Deal.Modules.Projects/` | каталога нет; есть `Deal.Modules.Telegram/` | ✅ исправлено на `Deal.Modules.Telegram` |
|
||||
| 2 | §3 «Модули core» (таблица, ~L74) | «Выбранные» владеет `Deal.Modules.Projects`; нет Telegram | сервисы «Выбранных» — в `Deal.Modules.Kanban` (`CardsService.Selected`); модуль `Deal.Modules.Telegram` существует | ✅ исправлено + добавлена строка Telegram |
|
||||
| 3 | §3 (абзац, ~L80) | «`Projects` — сервисами пространства…» | модуля `Projects` нет (перенесено в Kanban) | ✅ исправлено |
|
||||
| 4 | §4 «Ключевые таблицы public» (~L109-112) | `tenants(…, limits_json)`, `users(…, email, role)`, `invites(id, tenant_id, email, code, expires_at, used_at)`, `app_settings` | `tenants(Id,Name,Status,CreatedAt)`, `users(…,Login,…)`, `invites(Code PK,Email,TenantId,Status,ExpiresAt,ActivatedAt,CreatedById,CreatedAt)`, `global_settings`; таблицы `app_settings` нет | ✅ исправлено |
|
||||
| 5 | §4 сноска (~L122) | `Operators`, `OperatorSessions` | таблицы — `operators`, `operator_sessions` (миграция `SystemSaaS`) | ✅ исправлено |
|
||||
| 6 | §6 «Файлы» (~L202) | ключ объекта = `tenant_<id>/<card_id>/<file_id>` | `CardsService` строит `projects/<card_id>/<file_id>_<unixMs>_<safeName>` | ✅ исправлено |
|
||||
| 7 | §8 «Развёртывание» (сноска, ~L304) | «корневой `docker-compose.yml` — наследие LeadRadar» | файл перенесён в `archive/leadradar-legacy/`; в корне его нет | ✅ исправлено |
|
||||
| 8 | §11 этап 5 (~L510) | модуль/таблица `Deal.Modules.Projects`/`ProjectCards` без пометки | упразднены с этапа 9 | ✅ добавлена пометка «историческое состояние» |
|
||||
| 9 | §11 TODO (~L619-620) | «OpenAPI-карта снимается с LeadRadar», «миграции на 1000 схем — в плане этапа 0» | api-map и контракты есть; пакетная миграция реализована (этап 12) | ✅ исправлено |
|
||||
| 10 | §13.4a (~L717) | секреты включают `tgKeys.apiHash` в настройках тенанта, маска `apiHashSet` | `tgKeys` у тенанта нет; ключи — у оператора (`global_settings`, `GET/PUT /api/operator/settings/telegram-keys`) | ✅ исправлено + пометка |
|
||||
| 11 | §13.5 «Проверка схем» (~L888) | схема тенанта содержит `Boards`, `ProjectCards`; public — неполный | `Boards`/`ProjectCards` удалены (этап 9); актуальны `Containers`, `Dialogs`, `Disc*` и т.д. | ✅ исправлено на актуальный список |
|
||||
| 12 | §13.6 «Тесты» (~L905) | `dotnet test` ожидает **1203 PASS** | актуальный core — **1275** | ✅ исправлено |
|
||||
| 13 | §13.7 env (~L976) | core в compose задаёт `DEAL_DEMO=1` | в `compose.dev.yml` `DEAL_DEMO` нет; демо-ручки удалены | ✅ исправлено |
|
||||
| 14 | §13.7 smoke (~L994) | `POST /api/demo/simulate-lead` → `/api/leads/{id}/trash` | `dev-smoke.sh`: `POST /api/cards` → `POST /api/cards/{id}/trash` | ✅ исправлено |
|
||||
| 15 | §13.7 ручные проверки (~L1061) | `PATCH /api/settings tgKeys` | ключи — у оператора (вариант A) | ✅ исправлено |
|
||||
| 16 | §13.8 (~L1074) | `public.Operators`/`OperatorSessions` | `operators`/`operator_sessions` | ✅ исправлено |
|
||||
| 17 | §13.8 (~L1111) | «приостановка тенанта (вход **401**…)» | вход приостановленного тенанта — **403** (`AuthEndpoints`) | ✅ исправлено |
|
||||
| 18 | §13 заголовок (~L636) | «актуально для этапов 0–10» | актуально по этап 12 | ✅ исправлено |
|
||||
| 19 | §13.4e (~L841) | исторический раздел этапа 5 без пометки | операции переехали в `/api/cards*`, модуль/таблица удалены | ✅ добавлена пометка |
|
||||
| 20 | §16 «Добивка» (~L1339) | core-тесты **1245/1245** | актуально **1275/1275** | ✅ исправлено |
|
||||
|
||||
### `docs/user-guide/Инструкция-пользователя-Дейл.md`
|
||||
|
||||
| # | Место | В доке | Реальность | Статус |
|
||||
|---|---|---|---|---|
|
||||
| 21 | §1 «Особенности» (~L32-34) | демо-кнопки («демо-карточка», «демо-сообщение») при `DEAL_DEMO=1` | во фронте демо-кнопок нет, ручки `POST /api/demo/*` и флаг удалены | ✅ исправлено |
|
||||
|
||||
### `docs/api/api-map.md`
|
||||
|
||||
| # | Место | В доке | Реальность | Статус |
|
||||
|---|---|---|---|---|
|
||||
| 22 | §3.1 (~L59) | `change-password` — минимум **4** символа | `AuthEndpoints` — минимум **8** | ✅ исправлено |
|
||||
| 23 | §4.1 (~L248) | `objectKey: "cards/c_…/pf_…"` | формат `projects/<cardId>/<fileId>_<ms>_<name>` | ✅ исправлено |
|
||||
| 24 | §5 «Прочие домены» (~L400) | Operator + join = **21** | 24 операторских ручки + `/api/join` = **25** | ✅ исправлено |
|
||||
|
||||
### `docs/superpowers/STATUS.md`
|
||||
|
||||
| # | Место | В доке | Реальность | Статус |
|
||||
|---|---|---|---|---|
|
||||
| 25 | (~L30) | core **1203/1203 PASS** | 1275 | ✅ исправлено |
|
||||
| 26 | (~L52) | «демо `DEAL_DEMO`» | демо удалено | ✅ исправлено |
|
||||
| 27 | (~L56) | `public.Operators/OperatorSessions` | `operators`/`operator_sessions` | ✅ исправлено |
|
||||
| 28 | (~L76) | «демо-пространство, `DEAL_DEMO=1`» | dev-seed `admin/admin`, демо удалено | ✅ исправлено |
|
||||
| 29 | (~L90) | «settings/boards/demo-карточка» (live-приёмка) | актуальные ручки — `/api/settings`, `/api/cards` | ✅ исправлено + историческая пометка |
|
||||
| 30 | (~L94) | «simulate-lead → карточка inbox» | `dev-smoke.sh`: `POST /api/cards` → карточка `planned` | ✅ исправлено |
|
||||
|
||||
> Нумерация строк приблизительная (после правок сместилась).
|
||||
|
||||
## Проверено и сходится (выборка)
|
||||
|
||||
- **Порты**: core HTTP 5080 / gRPC-ингресс 5082, telegram-service 5101, ai-service 5102,
|
||||
ml-service 5103, metrics 9464 (`METRICS_PORT`), postgres host-порт 5433, minio 9000/9001,
|
||||
grafana `127.0.0.1:3001`, prometheus `127.0.0.1:9090` — совпадают с `deploy/compose.*.yml`
|
||||
и Dockerfile.
|
||||
- **Команды**: `docker compose -f deploy/compose.dev.yml up -d --build`, `scripts/dev-smoke.sh`,
|
||||
`scripts/test.sh` (+ `npm run lint:i18n`), фронт `npm run dev` — совпадают.
|
||||
- **Единый API**: `/api/cards` + `/api/containers`; домены `/api/leads|projects|boards|columns`
|
||||
удалены — совпадает с `Endpoints/*` и `api-map`.
|
||||
- **Оператор-консоль**: hash-роутер `#/` / `#/operator` / `#/join?code=…` —
|
||||
`src/frontend/src/router.js`; ключи Telegram — `global_settings` + `OperatorSettingsEndpoints`.
|
||||
- **БД**: `Containers` вместо `Boards`, `ProjectCards` нет, `Cards` с модульными JSON-полями;
|
||||
публичные таблицы `audit_log`/`token_usage_events`/`global_settings`/`rate_limit_counters`
|
||||
и lowercase `operators`/`operator_sessions` — подтверждено EF-конфигами и миграциями.
|
||||
- **Файлы**: `objectKey = projects/<cardId>/<fileId>_<ms>_<name>` — `CardsService.Files`.
|
||||
- **Наблюдаемость**: `/metrics` на отдельном HTTP/1.1-эндпоинте :9464, Serilog, promtail/loki/grafana —
|
||||
подтверждено `DealMetricsHosting`, `compose.prod.yml`.
|
||||
|
||||
## Осталось / непроверяемое
|
||||
|
||||
- **Счётчики тестов сервисов** (telegram 125, ai 52, ml 38): перезапуск тестов не выполнялся
|
||||
(запрет на долгие процессы). В доках трогали только core-счётчик (1203/1245 → 1275) по
|
||||
ground-truth задания; сами цифры сервисов не подтверждались кодом.
|
||||
- **Точное число операторских ручек (25)** — подсчёт по `Endpoints/Operator*` + `JoinEndpoint`;
|
||||
группировка может отличаться от авторской (ранее было 21 — вероятно, до этапа 12).
|
||||
- **Исторические разделы-журналы** (§11 этапы 1–7, §13.4c/4d/4e, live-приёмки в STATUS/планах)
|
||||
намеренно сохраняют легаси-термины (`Boards`, `/api/leads`, `/api/projects`, `DEAL_DEMO`,
|
||||
`ProjectCards`). Добавлены точечные пометки «историческое состояние»; полный перепис
|
||||
не выполнялся (вне правил задачи).
|
||||
- **Планы/архитектурные доки** (`docs/superpowers/plans/*`, `docs/architecture/*`) содержат
|
||||
легаси-термины (`Boards`, `ProjectCards`, `docker-compose.yml`) — вне периметра аудита.
|
||||
- **Живые контуры** (Telegram-вход, реальные LLM-вызовы, mTLS-рукопожатие, backup/restore на
|
||||
docker-стеке) не проверялись — нужны креды/Docker; в доках они помечены ⚠ Manual.
|
||||
- **Дубли/внутренние противоречия**: техдок §11 этап 5 и §13.4e описывают снятый контур
|
||||
«Выбранных» как историю; при следующей редакции их, возможно, стоит свернуть в ссылку на §3.
|
||||
# Аудит документации «Дейл»: сверка с кодом/конфигами
|
||||
|
||||
> Исторический документ (аудит документации, 2026-09-10; следующий — `2026-09-11-docs-final-sweep.md`). Актуальное состояние — `docs/superpowers/STATUS.md` и `docs/technical/Техническая-документация-Дейл.md`.
|
||||
|
||||
> Дата: 2026-09-11
|
||||
> Проверено: `docs/spec/ТЗ-дейл-новая-архитектура.md`,
|
||||
> `docs/user-guide/Инструкция-пользователя-Дейл.md`,
|
||||
> `docs/technical/Техническая-документация-Дейл.md`,
|
||||
> `docs/api/api-map.md`, плюс `docs/superpowers/STATUS.md`.
|
||||
> Метод: сверка утверждений с кодом (`src/core/Deal.Api/Endpoints/*`,
|
||||
> `src/core/Deal.Infrastructure/**`, `src/frontend/src/**`, `src/{ai,ml,telegram}-service`),
|
||||
> конфигами (`deploy/compose.*.yml`, `appsettings*.json`) и скриптами (`scripts/*.sh`).
|
||||
> Докер не поднимался, тесты не перезапускались (см. «непроверяемое»).
|
||||
|
||||
## Сводка
|
||||
|
||||
- Найдено расхождений: **30** (по пунктам таблиц ниже).
|
||||
- Исправлено прямо в доках: **30**.
|
||||
- Значимые подтверждённые факты, с которыми доки сходятся: порты (core 5080/5082, telegram 5101,
|
||||
ai 5102, ml 5103, metrics 9464, postgres 5433, minio 9000/9001, grafana 3001), единые домены
|
||||
`/api/cards` + `/api/containers`, оператор-консоль `#/operator` и активация `#/join`,
|
||||
ключи Telegram — у оператора (`global_settings`), команды запуска.
|
||||
|
||||
## Расхождения (файл:строка → в доке → реальность → исправлено)
|
||||
|
||||
### `docs/technical/Техническая-документация-Дейл.md`
|
||||
|
||||
| # | Место | В доке | Реальность (код) | Статус |
|
||||
|---|---|---|---|---|
|
||||
| 1 | §2 «Структура» (~L43) | проект `Deal.Modules.Projects/` | каталога нет; есть `Deal.Modules.Telegram/` | ✅ исправлено на `Deal.Modules.Telegram` |
|
||||
| 2 | §3 «Модули core» (таблица, ~L74) | «Выбранные» владеет `Deal.Modules.Projects`; нет Telegram | сервисы «Выбранных» — в `Deal.Modules.Kanban` (`CardsService.Selected`); модуль `Deal.Modules.Telegram` существует | ✅ исправлено + добавлена строка Telegram |
|
||||
| 3 | §3 (абзац, ~L80) | «`Projects` — сервисами пространства…» | модуля `Projects` нет (перенесено в Kanban) | ✅ исправлено |
|
||||
| 4 | §4 «Ключевые таблицы public» (~L109-112) | `tenants(…, limits_json)`, `users(…, email, role)`, `invites(id, tenant_id, email, code, expires_at, used_at)`, `app_settings` | `tenants(Id,Name,Status,CreatedAt)`, `users(…,Login,…)`, `invites(Code PK,Email,TenantId,Status,ExpiresAt,ActivatedAt,CreatedById,CreatedAt)`, `global_settings`; таблицы `app_settings` нет | ✅ исправлено |
|
||||
| 5 | §4 сноска (~L122) | `Operators`, `OperatorSessions` | таблицы — `operators`, `operator_sessions` (миграция `SystemSaaS`) | ✅ исправлено |
|
||||
| 6 | §6 «Файлы» (~L202) | ключ объекта = `tenant_<id>/<card_id>/<file_id>` | `CardsService` строит `projects/<card_id>/<file_id>_<unixMs>_<safeName>` | ✅ исправлено |
|
||||
| 7 | §8 «Развёртывание» (сноска, ~L304) | «корневой `docker-compose.yml` — наследие LeadRadar» | файл перенесён в `archive/leadradar-legacy/`; в корне его нет | ✅ исправлено |
|
||||
| 8 | §11 этап 5 (~L510) | модуль/таблица `Deal.Modules.Projects`/`ProjectCards` без пометки | упразднены с этапа 9 | ✅ добавлена пометка «историческое состояние» |
|
||||
| 9 | §11 TODO (~L619-620) | «OpenAPI-карта снимается с LeadRadar», «миграции на 1000 схем — в плане этапа 0» | api-map и контракты есть; пакетная миграция реализована (этап 12) | ✅ исправлено |
|
||||
| 10 | §13.4a (~L717) | секреты включают `tgKeys.apiHash` в настройках тенанта, маска `apiHashSet` | `tgKeys` у тенанта нет; ключи — у оператора (`global_settings`, `GET/PUT /api/operator/settings/telegram-keys`) | ✅ исправлено + пометка |
|
||||
| 11 | §13.5 «Проверка схем» (~L888) | схема тенанта содержит `Boards`, `ProjectCards`; public — неполный | `Boards`/`ProjectCards` удалены (этап 9); актуальны `Containers`, `Dialogs`, `Disc*` и т.д. | ✅ исправлено на актуальный список |
|
||||
| 12 | §13.6 «Тесты» (~L905) | `dotnet test` ожидает **1203 PASS** | актуальный core — **1275** | ✅ исправлено |
|
||||
| 13 | §13.7 env (~L976) | core в compose задаёт `DEAL_DEMO=1` | в `compose.dev.yml` `DEAL_DEMO` нет; демо-ручки удалены | ✅ исправлено |
|
||||
| 14 | §13.7 smoke (~L994) | `POST /api/demo/simulate-lead` → `/api/leads/{id}/trash` | `dev-smoke.sh`: `POST /api/cards` → `POST /api/cards/{id}/trash` | ✅ исправлено |
|
||||
| 15 | §13.7 ручные проверки (~L1061) | `PATCH /api/settings tgKeys` | ключи — у оператора (вариант A) | ✅ исправлено |
|
||||
| 16 | §13.8 (~L1074) | `public.Operators`/`OperatorSessions` | `operators`/`operator_sessions` | ✅ исправлено |
|
||||
| 17 | §13.8 (~L1111) | «приостановка тенанта (вход **401**…)» | вход приостановленного тенанта — **403** (`AuthEndpoints`) | ✅ исправлено |
|
||||
| 18 | §13 заголовок (~L636) | «актуально для этапов 0–10» | актуально по этап 12 | ✅ исправлено |
|
||||
| 19 | §13.4e (~L841) | исторический раздел этапа 5 без пометки | операции переехали в `/api/cards*`, модуль/таблица удалены | ✅ добавлена пометка |
|
||||
| 20 | §16 «Добивка» (~L1339) | core-тесты **1245/1245** | актуально **1275/1275** | ✅ исправлено |
|
||||
|
||||
### `docs/user-guide/Инструкция-пользователя-Дейл.md`
|
||||
|
||||
| # | Место | В доке | Реальность | Статус |
|
||||
|---|---|---|---|---|
|
||||
| 21 | §1 «Особенности» (~L32-34) | демо-кнопки («демо-карточка», «демо-сообщение») при `DEAL_DEMO=1` | во фронте демо-кнопок нет, ручки `POST /api/demo/*` и флаг удалены | ✅ исправлено |
|
||||
|
||||
### `docs/api/api-map.md`
|
||||
|
||||
| # | Место | В доке | Реальность | Статус |
|
||||
|---|---|---|---|---|
|
||||
| 22 | §3.1 (~L59) | `change-password` — минимум **4** символа | `AuthEndpoints` — минимум **8** | ✅ исправлено |
|
||||
| 23 | §4.1 (~L248) | `objectKey: "cards/c_…/pf_…"` | формат `projects/<cardId>/<fileId>_<ms>_<name>` | ✅ исправлено |
|
||||
| 24 | §5 «Прочие домены» (~L400) | Operator + join = **21** | 24 операторских ручки + `/api/join` = **25** | ✅ исправлено |
|
||||
|
||||
### `docs/superpowers/STATUS.md`
|
||||
|
||||
| # | Место | В доке | Реальность | Статус |
|
||||
|---|---|---|---|---|
|
||||
| 25 | (~L30) | core **1203/1203 PASS** | 1275 | ✅ исправлено |
|
||||
| 26 | (~L52) | «демо `DEAL_DEMO`» | демо удалено | ✅ исправлено |
|
||||
| 27 | (~L56) | `public.Operators/OperatorSessions` | `operators`/`operator_sessions` | ✅ исправлено |
|
||||
| 28 | (~L76) | «демо-пространство, `DEAL_DEMO=1`» | dev-seed `admin/admin`, демо удалено | ✅ исправлено |
|
||||
| 29 | (~L90) | «settings/boards/demo-карточка» (live-приёмка) | актуальные ручки — `/api/settings`, `/api/cards` | ✅ исправлено + историческая пометка |
|
||||
| 30 | (~L94) | «simulate-lead → карточка inbox» | `dev-smoke.sh`: `POST /api/cards` → карточка `planned` | ✅ исправлено |
|
||||
|
||||
> Нумерация строк приблизительная (после правок сместилась).
|
||||
|
||||
## Проверено и сходится (выборка)
|
||||
|
||||
- **Порты**: core HTTP 5080 / gRPC-ингресс 5082, telegram-service 5101, ai-service 5102,
|
||||
ml-service 5103, metrics 9464 (`METRICS_PORT`), postgres host-порт 5433, minio 9000/9001,
|
||||
grafana `127.0.0.1:3001`, prometheus `127.0.0.1:9090` — совпадают с `deploy/compose.*.yml`
|
||||
и Dockerfile.
|
||||
- **Команды**: `docker compose -f deploy/compose.dev.yml up -d --build`, `scripts/dev-smoke.sh`,
|
||||
`scripts/test.sh` (+ `npm run lint:i18n`), фронт `npm run dev` — совпадают.
|
||||
- **Единый API**: `/api/cards` + `/api/containers`; домены `/api/leads|projects|boards|columns`
|
||||
удалены — совпадает с `Endpoints/*` и `api-map`.
|
||||
- **Оператор-консоль**: hash-роутер `#/` / `#/operator` / `#/join?code=…` —
|
||||
`src/frontend/src/router.js`; ключи Telegram — `global_settings` + `OperatorSettingsEndpoints`.
|
||||
- **БД**: `Containers` вместо `Boards`, `ProjectCards` нет, `Cards` с модульными JSON-полями;
|
||||
публичные таблицы `audit_log`/`token_usage_events`/`global_settings`/`rate_limit_counters`
|
||||
и lowercase `operators`/`operator_sessions` — подтверждено EF-конфигами и миграциями.
|
||||
- **Файлы**: `objectKey = projects/<cardId>/<fileId>_<ms>_<name>` — `CardsService.Files`.
|
||||
- **Наблюдаемость**: `/metrics` на отдельном HTTP/1.1-эндпоинте :9464, Serilog, promtail/loki/grafana —
|
||||
подтверждено `DealMetricsHosting`, `compose.prod.yml`.
|
||||
|
||||
## Осталось / непроверяемое
|
||||
|
||||
- **Счётчики тестов сервисов** (telegram 125, ai 52, ml 38): перезапуск тестов не выполнялся
|
||||
(запрет на долгие процессы). В доках трогали только core-счётчик (1203/1245 → 1275) по
|
||||
ground-truth задания; сами цифры сервисов не подтверждались кодом.
|
||||
- **Точное число операторских ручек (25)** — подсчёт по `Endpoints/Operator*` + `JoinEndpoint`;
|
||||
группировка может отличаться от авторской (ранее было 21 — вероятно, до этапа 12).
|
||||
- **Исторические разделы-журналы** (§11 этапы 1–7, §13.4c/4d/4e, live-приёмки в STATUS/планах)
|
||||
намеренно сохраняют легаси-термины (`Boards`, `/api/leads`, `/api/projects`, `DEAL_DEMO`,
|
||||
`ProjectCards`). Добавлены точечные пометки «историческое состояние»; полный перепис
|
||||
не выполнялся (вне правил задачи).
|
||||
- **Планы/архитектурные доки** (`docs/superpowers/plans/*`, `docs/architecture/*`) содержат
|
||||
легаси-термины (`Boards`, `ProjectCards`, `docker-compose.yml`) — вне периметра аудита.
|
||||
- **Живые контуры** (Telegram-вход, реальные LLM-вызовы, mTLS-рукопожатие, backup/restore на
|
||||
docker-стеке) не проверялись — нужны креды/Docker; в доках они помечены ⚠ Manual.
|
||||
- **Дубли/внутренние противоречия**: техдок §11 этап 5 и §13.4e описывают снятый контур
|
||||
«Выбранных» как историю; при следующей редакции их, возможно, стоит свернуть в ссылку на §3.
|
||||
|
||||
@@ -1,329 +1,329 @@
|
||||
# Аудит соответствия ТЗ «Дейл (Deal) — новая архитектура»
|
||||
|
||||
> Исторический документ (аудит соответствия ТЗ, 2026-09-10). Актуальное состояние — `docs/superpowers/STATUS.md` и `docs/technical/Техническая-документация-Дейл.md`.
|
||||
|
||||
> Дата: 2026-09-10
|
||||
> Проверяется: `docs/spec/ТЗ-дейл-новая-архитектура.md` (§1–§12) + расширенные требования этапов 8–12.
|
||||
> Метод: **только исходный код и артефакты репозитория** (`C:\telbase`). Док-документам на слово
|
||||
> не верим — каждое утверждение подкреплено файлом/символом. Единственное запущенное — линтер
|
||||
> `npm run lint:i18n` (быстрый, read-only); остальное не запускалось.
|
||||
> Проект не git; правок кода/доков не вносилось, создан только настоящий отчёт.
|
||||
|
||||
## Сводка
|
||||
|
||||
| Статус | Кол-во |
|
||||
|---|---|
|
||||
| ✅ реализовано | 131 |
|
||||
| ⚠️ частично | 12 |
|
||||
| ❌ отсутствует | 1 |
|
||||
| Всего проверено пунктов | 144 |
|
||||
|
||||
Топ-находок — в разделе «Найденные пропуски/расхождения».
|
||||
(Каждая строка таблицы = один проверяемый пункт ТЗ/расширенных требований.)
|
||||
|
||||
---
|
||||
|
||||
## §1. О продукте
|
||||
|
||||
| № | Требование | Статус | Доказательство |
|
||||
|---|---|---|---|
|
||||
| 1.1 | Приём сообщений из источников в реальном времени | ✅ | `src/telegram-service/Deal.Telegram/Dialogs/RealtimeListener.cs` (PushMessage + mark-read), `Hosting/RealtimeMonitorService.cs` |
|
||||
| 1.2 | Отсев мусора (реклама/скам/служебное/дубли/устаревшее) | ✅ | `PipelineRejectConstants.cs` (stage labels `stop/spam_ml/spam_ai/filter_ai/dup/stale`), `IncomingRules.cs`, `PipelineWorkerService.Checks.cs` |
|
||||
| 1.3 | Структурирование в карточки по профилю (сфера/стек/бюджет/локация) | ✅ | `Pipeline/AiCardMapper.cs`, `Parse/LocalFieldsParser.cs`, `PipelineCardWriter.cs` |
|
||||
| 1.4 | Раскладка по колонкам-фильтрам | ✅ | `Kanban/ColumnRules/ColumnRules.cs`, `CardsService` (ContainerAccepts) |
|
||||
| 1.5 | Самообучение на действиях (ML) | ✅ | `Kanban/CardsService.Operations.cs` (PushAsync на move/trash/restore), `MlOutboxFlushScheduler` |
|
||||
| 1.6 | Discovery — поиск/подключение источников | ✅ | `Deal.Modules.Discovery/*`, `DiscoveryWorkerService.Search/Evaluate/Join` |
|
||||
|
||||
## §2. Термины
|
||||
|
||||
| № | Требование | Статус | Доказательство |
|
||||
|---|---|---|---|
|
||||
| 2.1 | Тенант владеет схемой БД/настройками/ML | ✅ | `Data/TenantContext.cs`, модель на тенанта (`TenantDb` миграции), модель ML per-tenant (`ml.proto`, `data/ml/<tenantId>.sqlite`) |
|
||||
| 2.2 | Аккаунт Telegram (1 на тенанта) | ✅ | `Telegram/Sessions/TenantSession.cs` («1 аккаунт на тенанта») |
|
||||
| 2.3 | Источник (канал/группа/чат) | ✅ | `Deal.Modules.Telegram/Application/ITelegramStore.cs`, `DialogEntity` |
|
||||
| 2.4 | Сырое сообщение → очередь | ✅ | `Pipeline/Application/Models/QueuedMessage.cs`, `PipelineIngestService.cs` |
|
||||
| 2.5 | Карточка — ядро + модули | ✅ | `Deal.Modules.Cards/Application/Card.cs`, интерфейсы `IContentCard/IBudgetedCard/IContactCard/IFileCard/ITzCard/IRemindableCard/…` |
|
||||
| 2.6 | Типы источника (локально/ссылка/файл/Telegram/импорт/API/ИИ/составной) | ✅ | `Deal.Modules.Cards/Application/ILocalSource.cs`, `IWebSource.cs`, `ITelegramSource.cs`, `IApiSource.cs`, `IFileSource.cs`, `IRowSource.cs`, `IAiSource.cs`, `ICompositeSource.cs` |
|
||||
| 2.7 | Контейнер + политика | ✅ | `Kanban/Application/Models/ContainerPolicyDto.cs`, `ContainersService.cs` |
|
||||
| 2.8 | Отсев с причиной | ✅ | `Pipeline/Application/Models/RejectedItemDto.cs`, `PipelineProcessingService.Rejected` |
|
||||
|
||||
## §3. Роли и доступ
|
||||
|
||||
| № | Требование | Статус | Доказательство |
|
||||
|---|---|---|---|
|
||||
| 3.1 | Оператор: тенанты/инвайты/лимиты/health/impersonation с аудитом | ✅ | `Endpoints/Operator*`, `OperatorTenantsEndpoints.Impersonate`, `AuditEvents.ImpersonationStarted/Stopped` |
|
||||
| 3.2 | Тенант: вход по инвайту, пароль, TG-аккаунт, обработка, дашборд | ✅ | `Tenants/Application/JoinService.cs`, `AuthService.cs`, `Endpoints/JoinEndpoint.cs` |
|
||||
| 3.3 | Регистрация только по инвайту | ✅ | `IInviteStore`, `InviteCodeGenerator` (16 симв., 72 ч), публичной регистрации нет |
|
||||
| 3.4 | Логин email+пароль, email уникален в SaaS | ✅ | `AuthService`, `users` (public), уникальность email |
|
||||
| 3.5 | `tenantId` — в сессии | ✅ | `Models/SessionDto.cs`, кука `deal_session`; JWT не используется (сессии) — допустимо формулировкой «сессии/JWT» |
|
||||
| 3.6 | Вход оператора изолирован от тенантов | ✅ | `Configuration/OperatorCookieOptions.cs` (`deal_operator_session`), `OperatorAuthEndpoints` |
|
||||
|
||||
## §4. Подключение Telegram-аккаунта
|
||||
|
||||
| № | Требование | Статус | Доказательство |
|
||||
|---|---|---|---|
|
||||
| 4.1 | Оператор **глобально** задаёт `api_id`/`api_hash` | ⚠️ | Ключи хранятся в **настройке тенанта** `tgKeys` (`SettingsKeys.TgKeys`, `Deal.Api/Telegram/TelegramKeysService.cs`) и задаются в UI тенанта (`settings/TelegramTab.vue`). Глобальной (операторской) настройки/ручки нет — расхождение с §4.1/§8 |
|
||||
| 4.2 | Подключение: QR или телефон+код | ✅ | `TelegramTab.vue` (qr/phone/code/password), `TelegramEndpoints` (start-qr/start-phone/submit-code/password), `TenantSession.StartQrAsync` |
|
||||
| 4.3 | Сессия сохраняется, статус подключения показан | ✅ | `Sessions/SessionStore.cs`, `SessionFileCipher.cs` (AES-GCM), `TgStatusService`, `GET /api/tg/status` |
|
||||
| 4.4 | 1 аккаунт на тенанта (схема допускает расширение) | ✅ | `TenantSession` (один на тенанта), `SessionFarm` |
|
||||
| 4.5 | Список диалогов подтягивается при подключении и обновляется на экране + в фоне | ✅ | `Dialogs/RealtimeSweep.cs` (SyncDialogs каждые 30 с), `TelegramEndpoints` `/dialogs/refresh`, `ChannelsView.vue` |
|
||||
| 4.6 | Вкл/выкл мониторинга по источнику | ✅ | `DialogsService.SetMonitorAsync`, `TelegramEndpoints` `/dialogs/{id}/monitor` |
|
||||
| 4.7 | «Новый чат → мониторинг автоматически» (вкл/выкл) | ✅ | `SettingsKeys.AutoMonitorNew`, `DialogsService.SyncFromTelegramAsync`, `TelegramStore.SyncFromTelegramAsync` |
|
||||
| 4.8 | Удалённые/покинутые источники исчезают | ✅ | `TelegramStore.SyncFromTelegramAsync` (удаление отсутствующих) |
|
||||
| 4.9 | «Перечитать»: догон ~10 сообщений включённых источников, анти-бан-паузы | ✅ | `Dialogs/BackfillService.cs` (`MessagesLimit=10`, паузы 1.5–3 с / 3–6 с), `POST /api/tg/dialogs/backfill-all` |
|
||||
| 4.10 | Полученные сообщения сразу помечаются прочитанными | ✅ | `RealtimeListener.OnMessageReceivedAsync` (MarkReadAsync после Push), `BackfillService` (read-ack) |
|
||||
| 4.11 | Discovery: задача поиска → ИИ ключевые слова | ✅ | `DiscoveryEndpoints` (generate-keywords), `IAiTools.GenerateKeywordsAsync` |
|
||||
| 4.12 | Поиск каналов, где аккаунт не состоит | ✅ | `DiscoveryWorkerService.Search.cs`, `IsDialogMonitoredAsync` |
|
||||
| 4.13 | Каскад: участники → язык → содержание (порог ≥40%) | ✅ | `DiscoveryWorkerService.Evaluate.cs`, `SettingsDefaults.DiscEvalThreshold = 40` |
|
||||
| 4.14 | Кандидаты «на рассмотрение» с метаданными/fit/темами/метками (закрытая группа) | ✅ | `DiscoveryWorkerService.Constants.cs` (`MarkClosedGroup`…), `FinishReviewAsync`, `Models/DiscoveryTopicDto` |
|
||||
| 4.15 | Действия: вручную «Вступить» / авто-вступление с квотами (50/сутки, 50–70 с) | ✅ | `DiscoveryBanGuard.cs` (`DiscJoinLimit=50`), `DiscoveryPacer.cs` (`DiscJoinDelayMin/Max=50/70`) |
|
||||
| 4.16 | «Отклонить» → чёрный список; список исключает во всех задачах; снимается вручную | ✅ | `DiscoveryBlacklistService.cs`, `DiscoveryBlacklistList.vue`, `RemoveBlacklistAsync` |
|
||||
|
||||
## §5. Обработка входящих (пайплайн)
|
||||
|
||||
| № | Требование | Статус | Доказательство |
|
||||
|---|---|---|---|
|
||||
| 5.1 | Путь: источник → очередь → стоп-лист → дедуп → ML → ИИ → карточка | ✅ | `PipelineWorkerService.Pump.cs`, `SignificantPath`, `PipelineIngestService` |
|
||||
| 5.2 | Этап 1: минимальная длина текста | ✅ | `IncomingRules.Evaluate` (`KindLength`), `SettingsDefaults.MinLen=24` |
|
||||
| 5.3 | Этап 1: стоп-фразы (настраиваемый список) | ✅ | `SettingsKeys.StopPhrases`, `IncomingRules` (`KindStop`), `settings/StopTab.vue` |
|
||||
| 5.4 | Этап 1: отсев резюме соискателей (настройка) | ✅ | `SettingsKeys.BlockResumes/ResumeMarkers`, `IncomingRules` (`KindResume`, guard «резюме» при маркере найма) |
|
||||
| 5.5 | Этап 1: тип заявки (только вакансии / только заказы) | ✅ | `SettingsKeys.WantedType`, `IncomingRules` (`KindType`), `hireMarkers` |
|
||||
| 5.6 | Этап 1: дедуп (нормализованный хэш) | ✅ | `Parse/DedupHasher.cs`, `DedupEntries` (миграция `TenantPipeline`) |
|
||||
| 5.7 | Этап 1: устаревшее сообщение → отсев | ✅ | `PipelineWorkerService.Checks.cs` `IsStaleAsync` (`ArchiveAfterDays`) |
|
||||
| 5.8 | ML: уверена → решает сама (спам/колонка); не уверена → ИИ | ✅ | `PipelineWorkerService.Pump.cs` (`run.MlEnabled && !force`), `ml.proto` (take/label/margin) |
|
||||
| 5.9 | Возврат из отсева (force) идёт мимо ML к ИИ | ✅ | `Pump.cs` (`force` пропускает ML), `PipelineProcessingService.ReturnAsync` (`Force = true`) |
|
||||
| 5.10 | ИИ-фильтр: не про заявки → отсев; выключатель `aiFilterEnabled` | ✅ | `Pump.cs`, `SettingsKeys.AiFilterEnabled`, `AiFilterResultDto.Skipped` |
|
||||
| 5.11 | Классификация: структурированный разбор (компания/формат/задача/требования/плюсы/условия/бюджет/стек/контакты/тип) | ✅ | `Parse/ParsedCardContent.cs`, `AiCardMapper.cs`, `ai.proto` ClassifyReply |
|
||||
| 5.12 | Назначение колонки с проверкой правил | ✅ | `ContainerAccepts`, `AiCardLearning.cs`, `ColumnRules.cs` |
|
||||
| 5.13 | Глобальный фильтр «без суммы» отдельно для вакансий и заказов | ✅ | `SettingsKeys.BudgetRequiredHire/Order`, `PipelineWorkerService.Checks.cs` `SkipNoBudgetAsync` |
|
||||
| 5.14 | Глобальные исключения по ключевым словам/технологиям/бюджету/локации | ⚠️ | Глобальных настроек-исключений нет: в `SettingsKeys` только `StopPhrases` (стоп-фразы) и per-column `exclude` (`ColumnExclusions.cs`). Исключений «ключевые слова/технологии/бюджет/локация» отдельного глобального уровня не найдено |
|
||||
| 5.15 | Карточка — одна строка одной таблицы `Cards`; `ProjectCards` упразднена | ✅ | Миграция `TenantUnifiedCard.cs` (`DropTable("ProjectCards")` + `AddColumn` `StackJson/LinksJson/FilesJson/HistoryJson/TzText/Reminder…`) |
|
||||
| 5.16 | Комментарии — общая таблица `LeadComments` | ✅ | Миграция `TenantKanban.cs` (`LeadComments`), `KanbanStore.Comments.cs` |
|
||||
| 5.17 | Единый реестр контейнеров; пространства не пересекаются; «взять в работу» = смена контейнера | ✅ | `ContainerSpaces.cs`, `ContainersService.cs`, `CardsService.Selected.cs` (`TakeAsync`) |
|
||||
| 5.18 | Исходное сообщение хранится и доступно (открыть в Telegram / форматированно) | ✅ | `CardDrawer.vue` (`sourceMsg`, `tgSourceUrl`, `renderSourceMessage`), `ProcessingView.vue` |
|
||||
|
||||
## §6. Дашборд (канбан)
|
||||
|
||||
| № | Требование | Статус | Доказательство |
|
||||
|---|---|---|---|
|
||||
| 6.1 | Колонки: «Неразобранное», пользовательские, «Архив», «Корзина» | ✅ | `CardIds` (inbox/archive/trash), `ContainerKinds`, `KanbanColumns` |
|
||||
| 6.2 | Пользователь создаёт колонки; ИИ **предлагает** с обоснованием; принять/отклонить/переименовать | ✅ | `AiSuggestEndpoints`, `SuggestHeuristics.cs`, `ContainerColumn.vue` (`acceptSuggestedBoard`, `suggested` badge) |
|
||||
| 6.3 | Колонка = сложный набор фильтров (ключевые слова/стек/грейд/уровень/цена/бюджет/локация/тип + отрицательные) | ⚠️ | `ContainerRulesDto` содержит только `Mode/Direction/Keywords/Stack/Grade/Exclude/Budget`. Отдельных групп «уровень/цена/локация/тип» нет (частично покрыты `direction`/`keywords`); отрицательные — `exclude` ✅ |
|
||||
| 6.4 | При помещении указаны критерии попадания | ✅ | `ColumnRules.ComputeHits`, `MatchHitBuilder`, `MatchHitDto` |
|
||||
| 6.5 | Свежие сверху; drag&drop между колонками с обучением ML | ✅ | `KanbanStore.Cards.cs` (`OrderByDescending(ReceivedAt)`), `composables/dnd.js`, `PushAsync` on move |
|
||||
| 6.6 | Быстрые действия: комментарий, корзина, контакт, «открыть исходник» | ⚠️ | Комментарий/корзина/контакт — `Card.vue` (кнопки). «Открыть исходник» на самой карточке нет — только в `CardDrawer.vue` и `ProcessingView.vue` |
|
||||
| 6.7 | Виджеты-счётчики свёрнутых колонок; двигать/менять размер | ✅ | `Sidebar.vue`, `cards.js` (`cycleWidth`, `colExtra`, `reorder`), `COLUMN_WIDTHS` |
|
||||
| 6.8 | Архив: старше N дней (1–30), очистка через 90 дней | ✅ | `StorageTickService.cs`, `SettingsDefaults.ArchiveAfterDays=14 (кламп 1..30)`, `ArchiveClearDays=90` |
|
||||
| 6.9 | Корзина: очистка раз в 7 дней; возврат из архива/корзины | ✅ | `SettingsDefaults.TrashClearDays=7`, `CardsService.Operations.cs` (`RestoreCardAsync`) |
|
||||
| 6.10 | «Выбранные»: стадии Запланировано→…→Готово/Отложено | ✅ | `CardsDefaultContainers.cs` (planned/reply/agree/work/review/ready/hold) |
|
||||
| 6.11 | «Взять в работу» — переход в контейнер, не клон | ✅ | `CardsService.Selected.cs` `TakeAsync` |
|
||||
| 6.12 | Модули работы: комментарии/сумма/стек/контакты, ссылки, ТЗ, файлы (S3/MinIO), значки количества | ✅ | `CardsService.Files.cs`, `CardFileKind.cs`, `FileKindDetector.cs`, `CardDrawer.vue` |
|
||||
| 6.13 | Отложенные: напоминания (срок+время, календарь); выключатель; выключено → не срабатывают | ✅ | `CardsService.Reminders.cs` (`RemindersDisabledDetail`, snooze +24 ч), `HoldReminderDialog.vue`, `SettingsDefaults.RemindersEnabled` |
|
||||
| 6.14 | История движения — под спойлером | ✅ | `CardDrawer.vue` (`<details>` «История движения», `historyReversed`) |
|
||||
| 6.15 | Ручное создание карточки (пометка «создано локально») | ✅ | `CardDetailsEndpoints` `POST /api/cards`, `Local` флаг, `Card.vue`/`CardDrawer.vue` бейдж «Локальная» |
|
||||
| 6.16 | Терминальные зоны «Отклонено»/«Выполнено»; в архив/корзину дашборда не попадают | ✅ | `CardsDefaultContainers.finished/rejected` (terminal), `ContainerPolicyDto.IsTerminal`, `ClearRejected` |
|
||||
|
||||
## §7. Вкладка «Обработка»
|
||||
|
||||
| № | Требование | Статус | Доказательство |
|
||||
|---|---|---|---|
|
||||
| 7.1 | Очередь (этап 1 / ожидают ИИ) с автопрокруткой | ✅ | `ProcessingView.vue` (таймер-опрос ~2.6 с, статусы `etap-1-bez-ii`/`ozhidaet-ii`); «автопрокрутка» реализована как авто-обновление |
|
||||
| 7.2 | Отсев с причиной и источником решения (правила/ML/ИИ/система) + конкретная фраза | ✅ | `PipelineRejectConstants.cs` (`StageLabels`/`SourceLabels`), `RejectedItemDto` (`kw`, `reason`) |
|
||||
| 7.3 | Метаданные, «Открыть исходник», «Исходное сообщение (форматированно)» | ✅ | `ProcessingView.vue` (`metaRows`, `sourceUrl`, `srcHtml`) |
|
||||
| 7.4 | Полнотекстовый поиск по отсеву | ✅ | `PipelineEndpoints` `/rejected?q=` (FTS ∪ LIKE), `Store` поиск |
|
||||
| 7.5 | Возврат из отсева: причины игнорируются, ML/ИИ обучаются, причина возврата | ✅ | `PipelineProcessingService.ReturnAsync` (`Force=true`, `PushAsync(spam,−1.0)`, `returnReason`) |
|
||||
| 7.6 | Автоочистка отсева раз в 3 дня; ручная очистка | ✅ | `PipelineRejectConstants.RetentionDays=3`, `POST /pipeline/rejected/clear`, `DELETE /rejected/{id}` |
|
||||
| 7.7 | Счётчик обработки в боковой панели; отсев в панели не показывается | ✅ | `Sidebar.vue` (`state.pQueueCounts.total`), отсев — только внутри `ProcessingView.vue` |
|
||||
|
||||
## §8. Настройки тенанта
|
||||
|
||||
| № | Требование | Статус | Доказательство |
|
||||
|---|---|---|---|
|
||||
| 8.1 | Telegram: ключи приложения (**оператор**), подключение, авто-мониторинг | ⚠️ | Подключение/авто-мониторинг ✅ (`TelegramTab.vue`, `AutoMonitorNew`). Ключи — настройка **тенанта** `tgKeys`, а не глобальная операторская (см. §4.1) |
|
||||
| 8.2 | ИИ: провайдер (в т.ч. локальные), модель, ключ зашифрован | ✅ | `AiProviders.cs`, `SettingsService.PatchSecrets.cs` (`enc:`), `ISecretCipher` |
|
||||
| 8.3 | Промпты: базовый + свой; библиотека по сферам + «мои промпты» | ✅ | `PromptLibraryModal.vue` (`PROMPT_LIBRARY`/`PROMPT_CATEGORIES`, поиск), `SettingsService.PatchMyPrompts.cs`, `AiTab.vue` |
|
||||
| 8.4 | ИИ вкл/выкл; ИИ-фильтр вкл/выкл | ✅ | `SettingsKeys.AiEnabled/AiFilterEnabled`, `Pump.cs` |
|
||||
| 8.5 | ML: вкл/выкл, обучение на действиях, **проверка на сообщении/канале**, сброс, самооценка | ⚠️ | `mlEnabled`, обучение (`PushAsync`), predict (сообщение) ✅, сброс ✅ (`/api/ml/reset`), самооценка ✅ (`MlEvalDto`). **Проверка на канале не реализована**: `POST /api/ml/candidates` возвращает пустой список (заглушка), `POST /api/ml/apply` — всегда 404 (`MlEndpoints.cs:130–155`) |
|
||||
| 8.6 | Обработка: стоп-фразы, длина, резюме, тип, домен/ключи, маркеры найма/заказа | ✅ | `SettingsKeys.StopPhrases/MinLen/BlockResumes/WantedType/DomainKeywords/HireMarkers`, `StopTab.vue`/`ScopeTab.vue` |
|
||||
| 8.7 | Колонки: набор, правила, отрицательные фильтры, исключения | ✅ | `ContainersEndpoints`, `BoardRulesDialog.vue`, `ColumnExclusions.cs` (см. замечание 6.3 по составу групп) |
|
||||
| 8.8 | Валюта: целевая, источник (4 запроса/сутки), конвертация при приёме + пересчёт старых (кроме архива/корзины), USDT=USD | ✅ | `RatesService.cs` (`RatesFetchInterval` = 6 ч = 4/сутки; USDT→USD), `ConversionRecomputer.cs` (`ConversionExcludedCols` archive/trash) |
|
||||
| 8.9 | Хранение: срок архивации (1–30), очистка архива/корзины | ✅ | `StorageTickService.cs`, `SettingsDefaults.ArchiveAfterDays/ArchiveClearDays/TrashClearDays`, `StorageTab.vue` |
|
||||
| 8.10 | Уведомления и напоминания; отложенные — отдельно | ✅ | `NotifyTab.vue`, `SettingsKeys.RemindersEnabled`, `CardsService.Reminders.cs` |
|
||||
| 8.11 | Звук | ✅ | `NotifyTab.vue` (`soundOn`, `volume`, `testSound`), `utils.js` (Web Audio) — клиентская настройка, без серверного ключа |
|
||||
| 8.12 | Внешний вид | ❌ | В `SettingsView.vue` вкладок Telegram/AI/Storage/Stop/Scope/ML/Notify/Currency/Profile — раздела «Внешний вид» (тема/оформление) нет; `style.css` содержит единственную тёмную тему |
|
||||
|
||||
## §9. Лимиты (бюджет токенов)
|
||||
|
||||
| № | Требование | Статус | Доказательство |
|
||||
|---|---|---|---|
|
||||
| 9.1 | Бюджет токенов на LLM, период настраивается | ✅ | `TenantLimitDto` (`BudgetTokens`, `Period` month/day), `OperatorLimitUpdateRequest` |
|
||||
| 9.2 | ai-service оценивает вызов в токенах, списывает с бюджета | ✅ | `TokenUsageRecorder.cs`, `BudgetedAiClassifier.cs`, `BudgetedAiTools.cs`, `ai.proto` Usage |
|
||||
| 9.3 | При исчерпании: fallback + уведомление; приём не блокируется | ✅ | `BudgetedAiClassifier` (Local-фолбэк), `Warned80/NotifiedExhausted`, условия `pipeline` не блокируются |
|
||||
| 9.4 | Оператор видит расход и меняет бюджет | ✅ | `OperatorLimitsEndpoints` (`/limits`, `/tenants/{id}/limit`), `AnalyticsService` |
|
||||
|
||||
## §10. Админка оператора
|
||||
|
||||
| № | Требование | Статус | Доказательство |
|
||||
|---|---|---|---|
|
||||
| 10.1 | Тенанты: создание, инвайты, статус, лимиты, приостановка | ✅ | `OperatorTenantsEndpoints` (create/suspend/unsuspend), `OperatorInvitesEndpoints` |
|
||||
| 10.2 | Health всех сервисов и очередей | ⚠️ | Сервисы ✅ (`OperatorHealthEndpoints`, ml/ai/telegram по gRPC-пробам). «Очереди» в health нет — глубины очередей публикуются только в метриках (`Observability/DealMetricsCollector.cs` → `/metrics`) |
|
||||
| 10.3 | Аудит: входы/выходы, инвайты, impersonation, действия оператора и тенанта | ✅ | `AuditEvents.cs` (login/logout/invite/impersonation/card_*/container_*/settings/channels/telegram), `AuditService` |
|
||||
| 10.4 | Аналитика: расход токенов (день/тенант/провайдер/модель) + лента действий с фильтрами | ✅ | `AnalyticsService.TokensAsync` (groupBy), `OperatorAnalyticsEndpoints`, `AuditSection.vue`/`AnalyticsSection.vue` |
|
||||
| 10.5 | Подозрительная активность (по логам безопасности) | ⚠️ | Отдельного разбора/детектора подозрительной активности не найдено; есть счётчики неудачных входов в `AnalyticsService.OverviewAsync` (`failedLogins`) и общие Grafana-дашборды |
|
||||
| 10.6 | Метрики сервисов (Prometheus/Grafana) | ✅ | `DealMetricsHosting.cs` (`/metrics` :9464), `deploy/observability/prometheus.yml`, `prometheus-rules.yml`, Grafana-дашборды |
|
||||
| 10.7 | UI: `#/operator` и `#/join` | ✅ | `router.js`, `views/operator/OperatorConsole.vue`, `views/JoinView.vue` |
|
||||
|
||||
## §11. Нефункциональные требования
|
||||
|
||||
| № | Требование | Статус | Доказательство |
|
||||
|---|---|---|---|
|
||||
| 11.1 | Безопасность: TLS, mTLS между сервисами | ✅ | `scripts/mtls-certs.sh`, `MtlsCertificates.cs`, `compose.prod.yml` (`DEAL_MTLS_*`), `MtlsOptions.cs` |
|
||||
| 11.2 | Параметризованный SQL | ✅ | EF Core / Npgsql по всему `Deal.Infrastructure`; ручной SQL — параметризованный (`ExecuteSqlRawAsync` без конкатенации) |
|
||||
| 11.3 | IDOR/XSS/SSRF/CSRF | ✅ | IDOR — session+tenant-scope middleware; XSS — `renderSourceMessage` (экранирование); SSRF — `AiConnectionChecker.cs` (`IsPrivateEndpoint`, allowlist `AiProviders`), `CbrRateSource` (fixed URL); CSRF — `OriginGuardMiddleware.cs` + SameSite |
|
||||
| 11.4 | Argon2id | ✅ | `DefaultPasswordHasher.cs` (Isopoh Argon2, Variant Argon2id) |
|
||||
| 11.5 | Rate limiting (прокси + приложение), счётчики распределённые в БД | ✅ | `StoreBackedFixedWindowRateLimiter.cs`, `IRateLimitCounterStore` → `RateLimitCounterStore` (public.rate_limit_counters), `LoginAttemptGuard.cs`, `RateLimitPolicies.cs` |
|
||||
| 11.6 | Cloudflare | ⚠️ | В коде нет интеграции/конфигурации Cloudflare; edge — Caddy (`deploy/caddy/Caddyfile`, TLS `internal`). Требование внешнего периметра, вне репозитория |
|
||||
| 11.7 | Ежедневные бэкапы (Postgres/файлы/сессии), outbox для событий | ✅ | `scripts/backup.sh`/`restore.sh`/`deal-backup-lib.sh`; outbox — `MlOutboxQueue.cs`, `MlOutboxFlushScheduler.cs` |
|
||||
| 11.8 | Авто-очистки (retention аудита/лимитов/счётчиков), разлогин suspended | ✅ | `DataRetentionScheduler.cs`, `DataRetentionOptions.cs`; `AuthService.ResolveSessionAsync` (suspended → null) |
|
||||
| 11.9 | Наблюдаемость: логи → Loki, метрики OTel→Prometheus→Grafana + алерты, `token_usage_events` | ✅ | `Logging/DealLogging.cs`, `deploy/observability/{promtail,loki}.yml`, `prometheus-rules.yml`; миграция `AddTokenUsageEvents` |
|
||||
| 11.10 | Масштабируемость: модульный монолит + сервисы ml/ai/telegram; k8s позже | ✅ | `Deal.Modules.*`, отдельные проекты `src/{ai,ml,telegram}-service`, `compose.*.yml`; k8s отсутствует (заявлено позже) |
|
||||
| 11.11 | Производительность: без потерь; анти-бан-паузы не блокируют обработку | ✅ | `PipelineIngestService`/`DedupEntries`, фоновые `PipelineWorkerScheduler`/`BackfillService`, `progressive.js` |
|
||||
| 11.12 | i18n: строки вынесены, RU по умолчанию, новые языки, переключение на лету с сохранением, форматтеры дат/чисел/валют, фолбэк RU | ⚠️ | Ядро i18n есть (`i18n/index.js`, `ru.js`/`ru.data.js`, `$t`), линтер проходит зелёным (проверено: `npm run lint:i18n` → ✓). Но: **нет UI-переключателя языка, нет второго языка и нет сохранения выбора** (в `index.js` прямо: «UI-переключателя на этом этапе нет»); даты/числа форматируются жёстко через `toLocale*('ru-RU', …)` (`store/core.js`, `store/settings.js`, `fmtNum` в `store/operator.js`), а не через locale-aware i18n-форматтеры |
|
||||
|
||||
## §12. Ограничения и допущения
|
||||
|
||||
| № | Требование | Статус | Доказательство |
|
||||
|---|---|---|---|
|
||||
| 12.1 | Фронтенд Vue 3 + Vite + Tailwind; единый контракт `/api/cards` + `/api/containers` с этапа 9 | ✅ | `package.json` (vue/vite/tailwind), `api.js`, `CardsEndpoints.cs`, `ContainersEndpoints.cs` |
|
||||
| 12.2 | Данные LeadRadar тестовые — не мигрируются | ✅ | Отдельные миграции Deal; данных-миграций из LeadRadar нет |
|
||||
| 12.3 | Kafka, k8s, биллинг-провайдер, саморегистрация — вне рамок | ✅ | В коде отсутствуют |
|
||||
| 12.4 | 1 Telegram-аккаунт на тенанта; несколько — позже | ✅ | `TenantSession` (1 на тенанта) |
|
||||
|
||||
---
|
||||
|
||||
## Расширенные требования (этапы 8–12)
|
||||
|
||||
| № | Требование | Статус | Доказательство |
|
||||
|---|---|---|---|
|
||||
| E1 | Библиотека готовых промптов по специальностям | ✅ | `ru.data.js` `PROMPT_LIBRARY` (IT/дизайн/недвижимость/стройка/услуги/красота/обучение), `PromptLibraryModal.vue` |
|
||||
| E2 | Категории и поиск в библиотеке | ✅ | `PROMPT_CATEGORIES`, фильтр `query`/`cat` в `PromptLibraryModal.vue` |
|
||||
| E3 | Раздел «Мои промпты» + свой промпт | ✅ | `SettingsService.PatchMyPrompts.cs`, `AiTab.vue` (`addMyPrompt`/`removeMyPrompt`), лимит ≤100 |
|
||||
| E4 | Двухэтапный стоп-лист: стоп-фразы без ИИ, затем ИИ-фильтр с возможностью отключить | ✅ | Этап 1 `IncomingRules` (без ИИ); ИИ-фильтр `FilterSafelyAsync` под `AiFilterEnabled` |
|
||||
| E5 | Исключения внутри колонки | ✅ | `ColumnExclusions.cs` (veto `Exclude`), `BoardRulesDialog.vue` |
|
||||
| E6 | Discovery: поиск/вступление в каналы и группы | ✅ | `DiscoveryWorkerService.Search/Join`, `DiscoveryOps` (telegram-service) |
|
||||
| E7 | Discovery: квоты/интервалы, закрытые группы, темы, список на рассмотрение | ✅ | `DiscoveryBanGuard`, `DiscoveryPacer`, `MarkClosedGroup`, `DiscoveryTopicGroup`, статус `review` |
|
||||
| E8 | «Перечитать каналы»/backfill, пометка прочитанными, мгновенный приём | ✅ | `BackfillService.cs` (10 сообщений, паузы), read-ack; `RealtimeListener.cs` |
|
||||
| E9 | ML отдельным контейнером | ✅ | `src/ml-service/Deal.Ml/Dockerfile` + `compose.dev.yml`/`compose.prod.yml` (`ml-service`, gRPC :5103) |
|
||||
| E10 | ML: обучение на действиях пользователя **и** ИИ | ✅ | Пользователь — `CardsService.Operations.cs` (`PushAsync(…,1.0)`); ИИ — `AiCardLearning.cs`, `CardReclassifier.cs` (`AiPushWeight`) |
|
||||
| E11 | Отдельная настройка проверки ML на сообщении/канале | ⚠️ | Проверка на **сообщении** ✅ (`POST /api/ml/predict`, `MLPanel.vue`); проверка на **канале** ❌ (`/api/ml/candidates` — пустая заглушка, `/api/ml/apply` — 404) |
|
||||
| E12 | Архив/корзина (сроки, возврат, ручная очистка) | ✅ | `StorageTickService.cs`, `CardsService.Operations.cs`, `clear-col`/`DELETE`, `Restore` |
|
||||
| E13 | Напоминания «Отложено» (календарь, отключение) | ✅ | `HoldReminderDialog.vue`, `CardsService.Reminders.cs`, `RemindersEnabled` |
|
||||
| E14 | История карточки под спойлером | ✅ | `CardDrawer.vue` `<details>` «История движения» |
|
||||
| E15 | Контакты квалифицированные (tg/phone/email/linkedin/site) | ✅ | `Parse/ContactsQualifier.cs` (типы `tg/phone/email/linkedin/whatsapp/site`, дедуп, отбой ботов/сервисных ссылок) |
|
||||
| E16 | «Открыть исходник» | ✅ | `CardDrawer.vue` (`sourceUrl`), `ProcessingView.vue` |
|
||||
| E17 | Источник не на карточке (только в деталях) | ✅ | `Card.vue` показывает лишь бейдж «Локальная»/контакты; канал и исходное сообщение — в `CardDrawer.vue` |
|
||||
| E18 | Бюджет: диапазон/вакансия/валюта + конвертация (4 раза в сутки) | ✅ | `CardBudget.cs`, `BudgetNormalizer.cs`, `RatesService.cs` (6 ч = 4/сутки), `ConversionRecomputer.cs` |
|
||||
| E19 | Обязательность суммы (опционально для вакансий) | ✅ | `SettingsKeys.BudgetRequiredHire/BudgetRequiredOrder`, `SkipNoBudgetAsync` |
|
||||
| E20 | Вкладка «Обработка» (очередь + отсев + причины + поиск) | ✅ | `ProcessingView.vue`, `PipelineEndpoints` |
|
||||
| E21 | Возврат из отсева с обучением | ✅ | `PipelineProcessingService.ReturnAsync` (`PushAsync(spam,−1.0)`, `Force`) |
|
||||
| E22 | Оператор-консоль | ✅ | `views/operator/*` (Tenants/Invites/Limits/Audit/Analytics/Health), `router.js` |
|
||||
| E23 | Аналитика токенов | ✅ | `AnalyticsService.cs`, `OperatorAnalyticsEndpoints.cs`, `token_usage_events` |
|
||||
| E24 | Аудит входов/выходов/действий (этап 10) | ✅ | `AuditEvents.cs`, `AuditService.cs`, `AuditSection.vue` |
|
||||
| E25 | i18n (вынос строк) | ⚠️ | Строки вынесены и линтер зелёный, но нет переключателя языка/второго языка/персистентности и locale-форматтеров (см. 11.12) |
|
||||
| E26 | Метрики Prometheus | ✅ | `DealMetricsHosting.cs`, `SharedKernel/Observability/DealMetrics.cs`, `prometheus.yml` (таргеты 5/5) |
|
||||
| E27 | Распределённый rate-limit | ✅ | `RateLimitCounterStore.cs` (Postgres), `StoreBackedFixedWindowRateLimiter.cs`, миграция `RateLimitCounters` |
|
||||
| E28 | reclassify (реальный, этап 12) | ✅ | `CardsEndpoints` `/reclassify` и `/{id}/reclassify`, `CardReclassifier.cs` (локальный фолбэк), `ReclassifyGate.cs`, audit `card_reclassified` |
|
||||
|
||||
---
|
||||
|
||||
## Найденные пропуски/расхождения
|
||||
|
||||
### ❌ Отсутствует
|
||||
|
||||
1. **§8.12 «Внешний вид» (настройки оформления).** В `SettingsView.vue` нет вкладки/раздела внешнего вида;
|
||||
тема одна (тёмная, `style.css` `@theme`). Отдельной настройки «внешний вид» не найдено.
|
||||
|
||||
### ⚠️ Частично
|
||||
|
||||
2. **§8.5 / E11 «проверка ML на канале».** `POST /api/ml/candidates` (`MlEndpoints.cs:131–141`) возвращает
|
||||
`{items: []}` с комментарием «До этапа 6 telegram-данных нет» — устаревшая заглушка; `POST /api/ml/apply`
|
||||
(`MlEndpoints.cs:144–155`) всегда отвечает 404 «Исходное сообщение не найдено». Реального разбора
|
||||
сообщений канала/ручного применения решения ML нет, хотя telegram-данные в системе уже есть
|
||||
(проверка на сообщении — `POST /api/ml/predict` — работает).
|
||||
3. **§5.14 «Глобальные исключения по ключевым словам/технологиям/бюджету/локации».** Глобальных настроек
|
||||
такого исключения в `SettingsKeys` нет: есть только `stopPhrases` (стоп-фразы) и per-column `exclude`
|
||||
(`ColumnExclusions.cs`). Исключения уровня «технология/бюджет/локация» как общий фильтр не найдены.
|
||||
4. **§4.1/§8.1 ключи Telegram.** Хранятся как настройка тенанта `tgKeys` (`TelegramKeysService.cs`) и
|
||||
вводятся в UI тенанта (`TelegramTab.vue`). ТЗ требует, чтобы `api_id`/`api_hash` задавал **оператор
|
||||
глобально** — глобальной операторской настройки/ручки нет.
|
||||
5. **§11.12 / E25 i18n.** Строки вынесены в словари (`i18n/locales/ru.js`, `ru.data.js`), `npm run lint:i18n`
|
||||
проходит. Но отсутствуют: UI-переключатель языка, второй язык, сохранение выбора, «переключение на лету»
|
||||
(в `i18n/index.js` явно сказано «UI-переключателя на этом этапе нет»). Форматирование дат/чисел жёстко
|
||||
`ru-RU` (`store/core.js:232–276`, `store/settings.js:351–406`, `store/operator.js:356`), не через
|
||||
locale-aware i18n-форматтеры.
|
||||
6. **§6.3 состав фильтров колонки.** `ContainerRulesDto` = `Mode/Direction/Keywords/Stack/Grade/Exclude/Budget`.
|
||||
ТЗ перечисляет также «уровень/цена/локация/тип» отдельными опциями — явных групп нет (частично
|
||||
покрываются `direction`/`keywords`).
|
||||
7. **§6.6 «открыть исходник» как быстрое действие карточки.** На `Card.vue` есть комментарий/корзина/контакт,
|
||||
но ссылки «открыть исходник» нет — она доступна только в `CardDrawer.vue` и `ProcessingView.vue`.
|
||||
8. **§10.2 health очередей.** `/api/operator/health` проверяет БД и сервисы ml/ai/telegram, но глубины
|
||||
очередей (пайплайн, MlOutbox) в JSON health не отдаёт — они только в метриках
|
||||
(`DealMetricsCollector.cs` → `/metrics`).
|
||||
9. **§10.5 подозрительная активность.** Специализированного детектора/ленты подозрительной активности по
|
||||
логам безопасности не найдено; есть лишь счётчик `failedLogins` в обзорной аналитике и общие
|
||||
Grafana-дашборды.
|
||||
10. **§11.6 Cloudflare.** В репозитории нет конфигурации/интеграции Cloudflare (edge — Caddy,
|
||||
`deploy/caddy/Caddyfile`). Требование периметра, вне кода приложения.
|
||||
11. **§7.1 «автопрокрутка» очереди.** Реализована как периодическое авто-обновление списка (~2.6 с,
|
||||
`ProcessingView.vue`), а не как буквальная авто-прокрутка. Семантически покрывает требование, но не
|
||||
дословно.
|
||||
|
||||
### Дефекты/легаси, замеченные при проверке (не пункты ТЗ, но влияют на заявленные функции)
|
||||
|
||||
12. **`NotifyTab.vue` — сломан список активных напоминаний.** `const holdReminders = computed(() => state.projectCards.filter(...))`
|
||||
(`settings/NotifyTab.vue:6–8`), при этом `state.projectCards` больше нигде в `src/` не определяется
|
||||
(grep даёт ровно одно совпадение — этот файл). После этапа 9 (`projectCards`/`stage` упразднены) обращение
|
||||
к `state.projectCards.filter` даёт `undefined.filter` → ошибка рендера вкладки «Уведомления».
|
||||
13. **Легаси-артефакты LeadRadar.** В корне остались `docker-compose.yml` (сервисы `app`/`ml`/`minio`
|
||||
старого стека), каталог `backend/` (python `app/`) и `mlservice/` (python). Текущая архитектура — `deploy/compose.*.yml`
|
||||
+ `src/{core,ai,ml,telegram}-service`. Прямого нарушения ТЗ нет, но это риск путаницы (в STATUS.md
|
||||
«судьба legacy `docker-compose.yml`» помечена как открытый вопрос).
|
||||
|
||||
---
|
||||
|
||||
## Чего проверка не покрывает
|
||||
|
||||
- **Живые внешние интеграции без кредов.** Реальный Telegram-вход (`api_id`/`api_hash`/QR) и реальные
|
||||
LLM-вызовы не проверялись (нет кредов; см. STATUS.md, п.5 «нужны живые креды»). Проверяется только
|
||||
наличие кода/контрактов и локальных заглушек.
|
||||
- **Живой контур Docker/k8s, mTLS-рукопожатие, Grafana/Loki/Prometheus.** Проверены конфиги
|
||||
(`compose.*.yml`, `deploy/observability/*`) и код обвязки, но не факт поднятия/скрейпа в этой сессии
|
||||
(сервисы не поднимались).
|
||||
- **Скрипты бэкапа/восстановления и нагрузочные тесты.** Наличие и читаемость проверены (`scripts/backup.sh`,
|
||||
`scripts/restore.sh`, `scripts/loadtest/`), но не выполнялись.
|
||||
- **Корректность чисел в тестах.** Тест-счётчики (STATUS.md: core 1203 и т.п.) не пересчитывались —
|
||||
тесты не запускались (кроме быстрого `lint:i18n`).
|
||||
- **UI-поведение в браузере.** Выводы по фронту основаны на чтении `.vue`/`.js`; реальные клики,
|
||||
drag&drop и рендер не воспроизводились.
|
||||
- **Внешний периметр (Cloudflare, TLS в проде, DNS, egress-контроль).** Вне репозитория.
|
||||
- **Соответствие формальным юридическим требованиям/биллингу** — вне рамок ТЗ (заявлено как «позже»).
|
||||
|
||||
---
|
||||
|
||||
## Обновление (2026-09-10, вечер) — статус после добивки
|
||||
|
||||
Часть найденных ⚠️/❌ закрыта в тот же день (детали — `.superpowers/sdd/deal-stage12-observability-hardening/task-tz-*.md`):
|
||||
|
||||
| Пункт | Было | Стало |
|
||||
|---|---|---|
|
||||
| §8.12 «Внешний вид» | ❌ | ✅ раздел настроек + темы тёмная/светлая/системная (§15 техдока) |
|
||||
| §8/E11 ML-проверка на канале | ⚠️ заглушка | ✅ `MlReviewService` (`/api/ml/candidates|apply`) |
|
||||
| §5.14 глобальные исключения | ⚠️ | ✅ `excludeKeywords/Locations/Types/Budget*` на стоп-этапе |
|
||||
| §6.3 группы фильтров колонки | ⚠️ | ✅ `levels/locations/types/prices` + matchHits |
|
||||
| §6.6 «открыть исходник» на карточке | ⚠️ | ✅ быстрое действие в `Card.vue` |
|
||||
| §10.2 health очередей | ⚠️ | ✅ `queues`/`sessions` в `/api/operator/health` |
|
||||
| §10.5 подозрительная активность | ⚠️ | ✅ `SuspiciousActivityService` + `/api/operator/analytics/suspicious` |
|
||||
|
||||
Остаются требующими владельца/кредов (осознанно): глобальные Telegram-ключи оператора (§4.1/§8.1),
|
||||
переключатель языка (§11.12 — **в бэклоге**, по потребности), живые Telegram/LLM-вызовы, Cloudflare/прод-периметр.
|
||||
Итог после добивки: core-тесты **1245/1245**; фронт build + `lint:i18n` зелёные.
|
||||
# Аудит соответствия ТЗ «Дейл (Deal) — новая архитектура»
|
||||
|
||||
> Исторический документ (аудит соответствия ТЗ, 2026-09-10). Актуальное состояние — `docs/superpowers/STATUS.md` и `docs/technical/Техническая-документация-Дейл.md`.
|
||||
|
||||
> Дата: 2026-09-10
|
||||
> Проверяется: `docs/spec/ТЗ-дейл-новая-архитектура.md` (§1–§12) + расширенные требования этапов 8–12.
|
||||
> Метод: **только исходный код и артефакты репозитория** (`C:\telbase`). Док-документам на слово
|
||||
> не верим — каждое утверждение подкреплено файлом/символом. Единственное запущенное — линтер
|
||||
> `npm run lint:i18n` (быстрый, read-only); остальное не запускалось.
|
||||
> Проект не git; правок кода/доков не вносилось, создан только настоящий отчёт.
|
||||
|
||||
## Сводка
|
||||
|
||||
| Статус | Кол-во |
|
||||
|---|---|
|
||||
| ✅ реализовано | 131 |
|
||||
| ⚠️ частично | 12 |
|
||||
| ❌ отсутствует | 1 |
|
||||
| Всего проверено пунктов | 144 |
|
||||
|
||||
Топ-находок — в разделе «Найденные пропуски/расхождения».
|
||||
(Каждая строка таблицы = один проверяемый пункт ТЗ/расширенных требований.)
|
||||
|
||||
---
|
||||
|
||||
## §1. О продукте
|
||||
|
||||
| № | Требование | Статус | Доказательство |
|
||||
|---|---|---|---|
|
||||
| 1.1 | Приём сообщений из источников в реальном времени | ✅ | `src/telegram-service/Deal.Telegram/Dialogs/RealtimeListener.cs` (PushMessage + mark-read), `Hosting/RealtimeMonitorService.cs` |
|
||||
| 1.2 | Отсев мусора (реклама/скам/служебное/дубли/устаревшее) | ✅ | `PipelineRejectConstants.cs` (stage labels `stop/spam_ml/spam_ai/filter_ai/dup/stale`), `IncomingRules.cs`, `PipelineWorkerService.Checks.cs` |
|
||||
| 1.3 | Структурирование в карточки по профилю (сфера/стек/бюджет/локация) | ✅ | `Pipeline/AiCardMapper.cs`, `Parse/LocalFieldsParser.cs`, `PipelineCardWriter.cs` |
|
||||
| 1.4 | Раскладка по колонкам-фильтрам | ✅ | `Kanban/ColumnRules/ColumnRules.cs`, `CardsService` (ContainerAccepts) |
|
||||
| 1.5 | Самообучение на действиях (ML) | ✅ | `Kanban/CardsService.Operations.cs` (PushAsync на move/trash/restore), `MlOutboxFlushScheduler` |
|
||||
| 1.6 | Discovery — поиск/подключение источников | ✅ | `Deal.Modules.Discovery/*`, `DiscoveryWorkerService.Search/Evaluate/Join` |
|
||||
|
||||
## §2. Термины
|
||||
|
||||
| № | Требование | Статус | Доказательство |
|
||||
|---|---|---|---|
|
||||
| 2.1 | Тенант владеет схемой БД/настройками/ML | ✅ | `Data/TenantContext.cs`, модель на тенанта (`TenantDb` миграции), модель ML per-tenant (`ml.proto`, `data/ml/<tenantId>.sqlite`) |
|
||||
| 2.2 | Аккаунт Telegram (1 на тенанта) | ✅ | `Telegram/Sessions/TenantSession.cs` («1 аккаунт на тенанта») |
|
||||
| 2.3 | Источник (канал/группа/чат) | ✅ | `Deal.Modules.Telegram/Application/ITelegramStore.cs`, `DialogEntity` |
|
||||
| 2.4 | Сырое сообщение → очередь | ✅ | `Pipeline/Application/Models/QueuedMessage.cs`, `PipelineIngestService.cs` |
|
||||
| 2.5 | Карточка — ядро + модули | ✅ | `Deal.Modules.Cards/Application/Card.cs`, интерфейсы `IContentCard/IBudgetedCard/IContactCard/IFileCard/ITzCard/IRemindableCard/…` |
|
||||
| 2.6 | Типы источника (локально/ссылка/файл/Telegram/импорт/API/ИИ/составной) | ✅ | `Deal.Modules.Cards/Application/ILocalSource.cs`, `IWebSource.cs`, `ITelegramSource.cs`, `IApiSource.cs`, `IFileSource.cs`, `IRowSource.cs`, `IAiSource.cs`, `ICompositeSource.cs` |
|
||||
| 2.7 | Контейнер + политика | ✅ | `Kanban/Application/Models/ContainerPolicyDto.cs`, `ContainersService.cs` |
|
||||
| 2.8 | Отсев с причиной | ✅ | `Pipeline/Application/Models/RejectedItemDto.cs`, `PipelineProcessingService.Rejected` |
|
||||
|
||||
## §3. Роли и доступ
|
||||
|
||||
| № | Требование | Статус | Доказательство |
|
||||
|---|---|---|---|
|
||||
| 3.1 | Оператор: тенанты/инвайты/лимиты/health/impersonation с аудитом | ✅ | `Endpoints/Operator*`, `OperatorTenantsEndpoints.Impersonate`, `AuditEvents.ImpersonationStarted/Stopped` |
|
||||
| 3.2 | Тенант: вход по инвайту, пароль, TG-аккаунт, обработка, дашборд | ✅ | `Tenants/Application/JoinService.cs`, `AuthService.cs`, `Endpoints/JoinEndpoint.cs` |
|
||||
| 3.3 | Регистрация только по инвайту | ✅ | `IInviteStore`, `InviteCodeGenerator` (16 симв., 72 ч), публичной регистрации нет |
|
||||
| 3.4 | Логин email+пароль, email уникален в SaaS | ✅ | `AuthService`, `users` (public), уникальность email |
|
||||
| 3.5 | `tenantId` — в сессии | ✅ | `Models/SessionDto.cs`, кука `deal_session`; JWT не используется (сессии) — допустимо формулировкой «сессии/JWT» |
|
||||
| 3.6 | Вход оператора изолирован от тенантов | ✅ | `Configuration/OperatorCookieOptions.cs` (`deal_operator_session`), `OperatorAuthEndpoints` |
|
||||
|
||||
## §4. Подключение Telegram-аккаунта
|
||||
|
||||
| № | Требование | Статус | Доказательство |
|
||||
|---|---|---|---|
|
||||
| 4.1 | Оператор **глобально** задаёт `api_id`/`api_hash` | ⚠️ | Ключи хранятся в **настройке тенанта** `tgKeys` (`SettingsKeys.TgKeys`, `Deal.Api/Telegram/TelegramKeysService.cs`) и задаются в UI тенанта (`settings/TelegramTab.vue`). Глобальной (операторской) настройки/ручки нет — расхождение с §4.1/§8 |
|
||||
| 4.2 | Подключение: QR или телефон+код | ✅ | `TelegramTab.vue` (qr/phone/code/password), `TelegramEndpoints` (start-qr/start-phone/submit-code/password), `TenantSession.StartQrAsync` |
|
||||
| 4.3 | Сессия сохраняется, статус подключения показан | ✅ | `Sessions/SessionStore.cs`, `SessionFileCipher.cs` (AES-GCM), `TgStatusService`, `GET /api/tg/status` |
|
||||
| 4.4 | 1 аккаунт на тенанта (схема допускает расширение) | ✅ | `TenantSession` (один на тенанта), `SessionFarm` |
|
||||
| 4.5 | Список диалогов подтягивается при подключении и обновляется на экране + в фоне | ✅ | `Dialogs/RealtimeSweep.cs` (SyncDialogs каждые 30 с), `TelegramEndpoints` `/dialogs/refresh`, `ChannelsView.vue` |
|
||||
| 4.6 | Вкл/выкл мониторинга по источнику | ✅ | `DialogsService.SetMonitorAsync`, `TelegramEndpoints` `/dialogs/{id}/monitor` |
|
||||
| 4.7 | «Новый чат → мониторинг автоматически» (вкл/выкл) | ✅ | `SettingsKeys.AutoMonitorNew`, `DialogsService.SyncFromTelegramAsync`, `TelegramStore.SyncFromTelegramAsync` |
|
||||
| 4.8 | Удалённые/покинутые источники исчезают | ✅ | `TelegramStore.SyncFromTelegramAsync` (удаление отсутствующих) |
|
||||
| 4.9 | «Перечитать»: догон ~10 сообщений включённых источников, анти-бан-паузы | ✅ | `Dialogs/BackfillService.cs` (`MessagesLimit=10`, паузы 1.5–3 с / 3–6 с), `POST /api/tg/dialogs/backfill-all` |
|
||||
| 4.10 | Полученные сообщения сразу помечаются прочитанными | ✅ | `RealtimeListener.OnMessageReceivedAsync` (MarkReadAsync после Push), `BackfillService` (read-ack) |
|
||||
| 4.11 | Discovery: задача поиска → ИИ ключевые слова | ✅ | `DiscoveryEndpoints` (generate-keywords), `IAiTools.GenerateKeywordsAsync` |
|
||||
| 4.12 | Поиск каналов, где аккаунт не состоит | ✅ | `DiscoveryWorkerService.Search.cs`, `IsDialogMonitoredAsync` |
|
||||
| 4.13 | Каскад: участники → язык → содержание (порог ≥40%) | ✅ | `DiscoveryWorkerService.Evaluate.cs`, `SettingsDefaults.DiscEvalThreshold = 40` |
|
||||
| 4.14 | Кандидаты «на рассмотрение» с метаданными/fit/темами/метками (закрытая группа) | ✅ | `DiscoveryWorkerService.Constants.cs` (`MarkClosedGroup`…), `FinishReviewAsync`, `Models/DiscoveryTopicDto` |
|
||||
| 4.15 | Действия: вручную «Вступить» / авто-вступление с квотами (50/сутки, 50–70 с) | ✅ | `DiscoveryBanGuard.cs` (`DiscJoinLimit=50`), `DiscoveryPacer.cs` (`DiscJoinDelayMin/Max=50/70`) |
|
||||
| 4.16 | «Отклонить» → чёрный список; список исключает во всех задачах; снимается вручную | ✅ | `DiscoveryBlacklistService.cs`, `DiscoveryBlacklistList.vue`, `RemoveBlacklistAsync` |
|
||||
|
||||
## §5. Обработка входящих (пайплайн)
|
||||
|
||||
| № | Требование | Статус | Доказательство |
|
||||
|---|---|---|---|
|
||||
| 5.1 | Путь: источник → очередь → стоп-лист → дедуп → ML → ИИ → карточка | ✅ | `PipelineWorkerService.Pump.cs`, `SignificantPath`, `PipelineIngestService` |
|
||||
| 5.2 | Этап 1: минимальная длина текста | ✅ | `IncomingRules.Evaluate` (`KindLength`), `SettingsDefaults.MinLen=24` |
|
||||
| 5.3 | Этап 1: стоп-фразы (настраиваемый список) | ✅ | `SettingsKeys.StopPhrases`, `IncomingRules` (`KindStop`), `settings/StopTab.vue` |
|
||||
| 5.4 | Этап 1: отсев резюме соискателей (настройка) | ✅ | `SettingsKeys.BlockResumes/ResumeMarkers`, `IncomingRules` (`KindResume`, guard «резюме» при маркере найма) |
|
||||
| 5.5 | Этап 1: тип заявки (только вакансии / только заказы) | ✅ | `SettingsKeys.WantedType`, `IncomingRules` (`KindType`), `hireMarkers` |
|
||||
| 5.6 | Этап 1: дедуп (нормализованный хэш) | ✅ | `Parse/DedupHasher.cs`, `DedupEntries` (миграция `TenantPipeline`) |
|
||||
| 5.7 | Этап 1: устаревшее сообщение → отсев | ✅ | `PipelineWorkerService.Checks.cs` `IsStaleAsync` (`ArchiveAfterDays`) |
|
||||
| 5.8 | ML: уверена → решает сама (спам/колонка); не уверена → ИИ | ✅ | `PipelineWorkerService.Pump.cs` (`run.MlEnabled && !force`), `ml.proto` (take/label/margin) |
|
||||
| 5.9 | Возврат из отсева (force) идёт мимо ML к ИИ | ✅ | `Pump.cs` (`force` пропускает ML), `PipelineProcessingService.ReturnAsync` (`Force = true`) |
|
||||
| 5.10 | ИИ-фильтр: не про заявки → отсев; выключатель `aiFilterEnabled` | ✅ | `Pump.cs`, `SettingsKeys.AiFilterEnabled`, `AiFilterResultDto.Skipped` |
|
||||
| 5.11 | Классификация: структурированный разбор (компания/формат/задача/требования/плюсы/условия/бюджет/стек/контакты/тип) | ✅ | `Parse/ParsedCardContent.cs`, `AiCardMapper.cs`, `ai.proto` ClassifyReply |
|
||||
| 5.12 | Назначение колонки с проверкой правил | ✅ | `ContainerAccepts`, `AiCardLearning.cs`, `ColumnRules.cs` |
|
||||
| 5.13 | Глобальный фильтр «без суммы» отдельно для вакансий и заказов | ✅ | `SettingsKeys.BudgetRequiredHire/Order`, `PipelineWorkerService.Checks.cs` `SkipNoBudgetAsync` |
|
||||
| 5.14 | Глобальные исключения по ключевым словам/технологиям/бюджету/локации | ⚠️ | Глобальных настроек-исключений нет: в `SettingsKeys` только `StopPhrases` (стоп-фразы) и per-column `exclude` (`ColumnExclusions.cs`). Исключений «ключевые слова/технологии/бюджет/локация» отдельного глобального уровня не найдено |
|
||||
| 5.15 | Карточка — одна строка одной таблицы `Cards`; `ProjectCards` упразднена | ✅ | Миграция `TenantUnifiedCard.cs` (`DropTable("ProjectCards")` + `AddColumn` `StackJson/LinksJson/FilesJson/HistoryJson/TzText/Reminder…`) |
|
||||
| 5.16 | Комментарии — общая таблица `LeadComments` | ✅ | Миграция `TenantKanban.cs` (`LeadComments`), `KanbanStore.Comments.cs` |
|
||||
| 5.17 | Единый реестр контейнеров; пространства не пересекаются; «взять в работу» = смена контейнера | ✅ | `ContainerSpaces.cs`, `ContainersService.cs`, `CardsService.Selected.cs` (`TakeAsync`) |
|
||||
| 5.18 | Исходное сообщение хранится и доступно (открыть в Telegram / форматированно) | ✅ | `CardDrawer.vue` (`sourceMsg`, `tgSourceUrl`, `renderSourceMessage`), `ProcessingView.vue` |
|
||||
|
||||
## §6. Дашборд (канбан)
|
||||
|
||||
| № | Требование | Статус | Доказательство |
|
||||
|---|---|---|---|
|
||||
| 6.1 | Колонки: «Неразобранное», пользовательские, «Архив», «Корзина» | ✅ | `CardIds` (inbox/archive/trash), `ContainerKinds`, `KanbanColumns` |
|
||||
| 6.2 | Пользователь создаёт колонки; ИИ **предлагает** с обоснованием; принять/отклонить/переименовать | ✅ | `AiSuggestEndpoints`, `SuggestHeuristics.cs`, `ContainerColumn.vue` (`acceptSuggestedBoard`, `suggested` badge) |
|
||||
| 6.3 | Колонка = сложный набор фильтров (ключевые слова/стек/грейд/уровень/цена/бюджет/локация/тип + отрицательные) | ⚠️ | `ContainerRulesDto` содержит только `Mode/Direction/Keywords/Stack/Grade/Exclude/Budget`. Отдельных групп «уровень/цена/локация/тип» нет (частично покрыты `direction`/`keywords`); отрицательные — `exclude` ✅ |
|
||||
| 6.4 | При помещении указаны критерии попадания | ✅ | `ColumnRules.ComputeHits`, `MatchHitBuilder`, `MatchHitDto` |
|
||||
| 6.5 | Свежие сверху; drag&drop между колонками с обучением ML | ✅ | `KanbanStore.Cards.cs` (`OrderByDescending(ReceivedAt)`), `composables/dnd.js`, `PushAsync` on move |
|
||||
| 6.6 | Быстрые действия: комментарий, корзина, контакт, «открыть исходник» | ⚠️ | Комментарий/корзина/контакт — `Card.vue` (кнопки). «Открыть исходник» на самой карточке нет — только в `CardDrawer.vue` и `ProcessingView.vue` |
|
||||
| 6.7 | Виджеты-счётчики свёрнутых колонок; двигать/менять размер | ✅ | `Sidebar.vue`, `cards.js` (`cycleWidth`, `colExtra`, `reorder`), `COLUMN_WIDTHS` |
|
||||
| 6.8 | Архив: старше N дней (1–30), очистка через 90 дней | ✅ | `StorageTickService.cs`, `SettingsDefaults.ArchiveAfterDays=14 (кламп 1..30)`, `ArchiveClearDays=90` |
|
||||
| 6.9 | Корзина: очистка раз в 7 дней; возврат из архива/корзины | ✅ | `SettingsDefaults.TrashClearDays=7`, `CardsService.Operations.cs` (`RestoreCardAsync`) |
|
||||
| 6.10 | «Выбранные»: стадии Запланировано→…→Готово/Отложено | ✅ | `CardsDefaultContainers.cs` (planned/reply/agree/work/review/ready/hold) |
|
||||
| 6.11 | «Взять в работу» — переход в контейнер, не клон | ✅ | `CardsService.Selected.cs` `TakeAsync` |
|
||||
| 6.12 | Модули работы: комментарии/сумма/стек/контакты, ссылки, ТЗ, файлы (S3/MinIO), значки количества | ✅ | `CardsService.Files.cs`, `CardFileKind.cs`, `FileKindDetector.cs`, `CardDrawer.vue` |
|
||||
| 6.13 | Отложенные: напоминания (срок+время, календарь); выключатель; выключено → не срабатывают | ✅ | `CardsService.Reminders.cs` (`RemindersDisabledDetail`, snooze +24 ч), `HoldReminderDialog.vue`, `SettingsDefaults.RemindersEnabled` |
|
||||
| 6.14 | История движения — под спойлером | ✅ | `CardDrawer.vue` (`<details>` «История движения», `historyReversed`) |
|
||||
| 6.15 | Ручное создание карточки (пометка «создано локально») | ✅ | `CardDetailsEndpoints` `POST /api/cards`, `Local` флаг, `Card.vue`/`CardDrawer.vue` бейдж «Локальная» |
|
||||
| 6.16 | Терминальные зоны «Отклонено»/«Выполнено»; в архив/корзину дашборда не попадают | ✅ | `CardsDefaultContainers.finished/rejected` (terminal), `ContainerPolicyDto.IsTerminal`, `ClearRejected` |
|
||||
|
||||
## §7. Вкладка «Обработка»
|
||||
|
||||
| № | Требование | Статус | Доказательство |
|
||||
|---|---|---|---|
|
||||
| 7.1 | Очередь (этап 1 / ожидают ИИ) с автопрокруткой | ✅ | `ProcessingView.vue` (таймер-опрос ~2.6 с, статусы `etap-1-bez-ii`/`ozhidaet-ii`); «автопрокрутка» реализована как авто-обновление |
|
||||
| 7.2 | Отсев с причиной и источником решения (правила/ML/ИИ/система) + конкретная фраза | ✅ | `PipelineRejectConstants.cs` (`StageLabels`/`SourceLabels`), `RejectedItemDto` (`kw`, `reason`) |
|
||||
| 7.3 | Метаданные, «Открыть исходник», «Исходное сообщение (форматированно)» | ✅ | `ProcessingView.vue` (`metaRows`, `sourceUrl`, `srcHtml`) |
|
||||
| 7.4 | Полнотекстовый поиск по отсеву | ✅ | `PipelineEndpoints` `/rejected?q=` (FTS ∪ LIKE), `Store` поиск |
|
||||
| 7.5 | Возврат из отсева: причины игнорируются, ML/ИИ обучаются, причина возврата | ✅ | `PipelineProcessingService.ReturnAsync` (`Force=true`, `PushAsync(spam,−1.0)`, `returnReason`) |
|
||||
| 7.6 | Автоочистка отсева раз в 3 дня; ручная очистка | ✅ | `PipelineRejectConstants.RetentionDays=3`, `POST /pipeline/rejected/clear`, `DELETE /rejected/{id}` |
|
||||
| 7.7 | Счётчик обработки в боковой панели; отсев в панели не показывается | ✅ | `Sidebar.vue` (`state.pQueueCounts.total`), отсев — только внутри `ProcessingView.vue` |
|
||||
|
||||
## §8. Настройки тенанта
|
||||
|
||||
| № | Требование | Статус | Доказательство |
|
||||
|---|---|---|---|
|
||||
| 8.1 | Telegram: ключи приложения (**оператор**), подключение, авто-мониторинг | ⚠️ | Подключение/авто-мониторинг ✅ (`TelegramTab.vue`, `AutoMonitorNew`). Ключи — настройка **тенанта** `tgKeys`, а не глобальная операторская (см. §4.1) |
|
||||
| 8.2 | ИИ: провайдер (в т.ч. локальные), модель, ключ зашифрован | ✅ | `AiProviders.cs`, `SettingsService.PatchSecrets.cs` (`enc:`), `ISecretCipher` |
|
||||
| 8.3 | Промпты: базовый + свой; библиотека по сферам + «мои промпты» | ✅ | `PromptLibraryModal.vue` (`PROMPT_LIBRARY`/`PROMPT_CATEGORIES`, поиск), `SettingsService.PatchMyPrompts.cs`, `AiTab.vue` |
|
||||
| 8.4 | ИИ вкл/выкл; ИИ-фильтр вкл/выкл | ✅ | `SettingsKeys.AiEnabled/AiFilterEnabled`, `Pump.cs` |
|
||||
| 8.5 | ML: вкл/выкл, обучение на действиях, **проверка на сообщении/канале**, сброс, самооценка | ⚠️ | `mlEnabled`, обучение (`PushAsync`), predict (сообщение) ✅, сброс ✅ (`/api/ml/reset`), самооценка ✅ (`MlEvalDto`). **Проверка на канале не реализована**: `POST /api/ml/candidates` возвращает пустой список (заглушка), `POST /api/ml/apply` — всегда 404 (`MlEndpoints.cs:130–155`) |
|
||||
| 8.6 | Обработка: стоп-фразы, длина, резюме, тип, домен/ключи, маркеры найма/заказа | ✅ | `SettingsKeys.StopPhrases/MinLen/BlockResumes/WantedType/DomainKeywords/HireMarkers`, `StopTab.vue`/`ScopeTab.vue` |
|
||||
| 8.7 | Колонки: набор, правила, отрицательные фильтры, исключения | ✅ | `ContainersEndpoints`, `BoardRulesDialog.vue`, `ColumnExclusions.cs` (см. замечание 6.3 по составу групп) |
|
||||
| 8.8 | Валюта: целевая, источник (4 запроса/сутки), конвертация при приёме + пересчёт старых (кроме архива/корзины), USDT=USD | ✅ | `RatesService.cs` (`RatesFetchInterval` = 6 ч = 4/сутки; USDT→USD), `ConversionRecomputer.cs` (`ConversionExcludedCols` archive/trash) |
|
||||
| 8.9 | Хранение: срок архивации (1–30), очистка архива/корзины | ✅ | `StorageTickService.cs`, `SettingsDefaults.ArchiveAfterDays/ArchiveClearDays/TrashClearDays`, `StorageTab.vue` |
|
||||
| 8.10 | Уведомления и напоминания; отложенные — отдельно | ✅ | `NotifyTab.vue`, `SettingsKeys.RemindersEnabled`, `CardsService.Reminders.cs` |
|
||||
| 8.11 | Звук | ✅ | `NotifyTab.vue` (`soundOn`, `volume`, `testSound`), `utils.js` (Web Audio) — клиентская настройка, без серверного ключа |
|
||||
| 8.12 | Внешний вид | ❌ | В `SettingsView.vue` вкладок Telegram/AI/Storage/Stop/Scope/ML/Notify/Currency/Profile — раздела «Внешний вид» (тема/оформление) нет; `style.css` содержит единственную тёмную тему |
|
||||
|
||||
## §9. Лимиты (бюджет токенов)
|
||||
|
||||
| № | Требование | Статус | Доказательство |
|
||||
|---|---|---|---|
|
||||
| 9.1 | Бюджет токенов на LLM, период настраивается | ✅ | `TenantLimitDto` (`BudgetTokens`, `Period` month/day), `OperatorLimitUpdateRequest` |
|
||||
| 9.2 | ai-service оценивает вызов в токенах, списывает с бюджета | ✅ | `TokenUsageRecorder.cs`, `BudgetedAiClassifier.cs`, `BudgetedAiTools.cs`, `ai.proto` Usage |
|
||||
| 9.3 | При исчерпании: fallback + уведомление; приём не блокируется | ✅ | `BudgetedAiClassifier` (Local-фолбэк), `Warned80/NotifiedExhausted`, условия `pipeline` не блокируются |
|
||||
| 9.4 | Оператор видит расход и меняет бюджет | ✅ | `OperatorLimitsEndpoints` (`/limits`, `/tenants/{id}/limit`), `AnalyticsService` |
|
||||
|
||||
## §10. Админка оператора
|
||||
|
||||
| № | Требование | Статус | Доказательство |
|
||||
|---|---|---|---|
|
||||
| 10.1 | Тенанты: создание, инвайты, статус, лимиты, приостановка | ✅ | `OperatorTenantsEndpoints` (create/suspend/unsuspend), `OperatorInvitesEndpoints` |
|
||||
| 10.2 | Health всех сервисов и очередей | ⚠️ | Сервисы ✅ (`OperatorHealthEndpoints`, ml/ai/telegram по gRPC-пробам). «Очереди» в health нет — глубины очередей публикуются только в метриках (`Observability/DealMetricsCollector.cs` → `/metrics`) |
|
||||
| 10.3 | Аудит: входы/выходы, инвайты, impersonation, действия оператора и тенанта | ✅ | `AuditEvents.cs` (login/logout/invite/impersonation/card_*/container_*/settings/channels/telegram), `AuditService` |
|
||||
| 10.4 | Аналитика: расход токенов (день/тенант/провайдер/модель) + лента действий с фильтрами | ✅ | `AnalyticsService.TokensAsync` (groupBy), `OperatorAnalyticsEndpoints`, `AuditSection.vue`/`AnalyticsSection.vue` |
|
||||
| 10.5 | Подозрительная активность (по логам безопасности) | ⚠️ | Отдельного разбора/детектора подозрительной активности не найдено; есть счётчики неудачных входов в `AnalyticsService.OverviewAsync` (`failedLogins`) и общие Grafana-дашборды |
|
||||
| 10.6 | Метрики сервисов (Prometheus/Grafana) | ✅ | `DealMetricsHosting.cs` (`/metrics` :9464), `deploy/observability/prometheus.yml`, `prometheus-rules.yml`, Grafana-дашборды |
|
||||
| 10.7 | UI: `#/operator` и `#/join` | ✅ | `router.js`, `views/operator/OperatorConsole.vue`, `views/JoinView.vue` |
|
||||
|
||||
## §11. Нефункциональные требования
|
||||
|
||||
| № | Требование | Статус | Доказательство |
|
||||
|---|---|---|---|
|
||||
| 11.1 | Безопасность: TLS, mTLS между сервисами | ✅ | `scripts/mtls-certs.sh`, `MtlsCertificates.cs`, `compose.prod.yml` (`DEAL_MTLS_*`), `MtlsOptions.cs` |
|
||||
| 11.2 | Параметризованный SQL | ✅ | EF Core / Npgsql по всему `Deal.Infrastructure`; ручной SQL — параметризованный (`ExecuteSqlRawAsync` без конкатенации) |
|
||||
| 11.3 | IDOR/XSS/SSRF/CSRF | ✅ | IDOR — session+tenant-scope middleware; XSS — `renderSourceMessage` (экранирование); SSRF — `AiConnectionChecker.cs` (`IsPrivateEndpoint`, allowlist `AiProviders`), `CbrRateSource` (fixed URL); CSRF — `OriginGuardMiddleware.cs` + SameSite |
|
||||
| 11.4 | Argon2id | ✅ | `DefaultPasswordHasher.cs` (Isopoh Argon2, Variant Argon2id) |
|
||||
| 11.5 | Rate limiting (прокси + приложение), счётчики распределённые в БД | ✅ | `StoreBackedFixedWindowRateLimiter.cs`, `IRateLimitCounterStore` → `RateLimitCounterStore` (public.rate_limit_counters), `LoginAttemptGuard.cs`, `RateLimitPolicies.cs` |
|
||||
| 11.6 | Cloudflare | ⚠️ | В коде нет интеграции/конфигурации Cloudflare; edge — Caddy (`deploy/caddy/Caddyfile`, TLS `internal`). Требование внешнего периметра, вне репозитория |
|
||||
| 11.7 | Ежедневные бэкапы (Postgres/файлы/сессии), outbox для событий | ✅ | `scripts/backup.sh`/`restore.sh`/`deal-backup-lib.sh`; outbox — `MlOutboxQueue.cs`, `MlOutboxFlushScheduler.cs` |
|
||||
| 11.8 | Авто-очистки (retention аудита/лимитов/счётчиков), разлогин suspended | ✅ | `DataRetentionScheduler.cs`, `DataRetentionOptions.cs`; `AuthService.ResolveSessionAsync` (suspended → null) |
|
||||
| 11.9 | Наблюдаемость: логи → Loki, метрики OTel→Prometheus→Grafana + алерты, `token_usage_events` | ✅ | `Logging/DealLogging.cs`, `deploy/observability/{promtail,loki}.yml`, `prometheus-rules.yml`; миграция `AddTokenUsageEvents` |
|
||||
| 11.10 | Масштабируемость: модульный монолит + сервисы ml/ai/telegram; k8s позже | ✅ | `Deal.Modules.*`, отдельные проекты `src/{ai,ml,telegram}-service`, `compose.*.yml`; k8s отсутствует (заявлено позже) |
|
||||
| 11.11 | Производительность: без потерь; анти-бан-паузы не блокируют обработку | ✅ | `PipelineIngestService`/`DedupEntries`, фоновые `PipelineWorkerScheduler`/`BackfillService`, `progressive.js` |
|
||||
| 11.12 | i18n: строки вынесены, RU по умолчанию, новые языки, переключение на лету с сохранением, форматтеры дат/чисел/валют, фолбэк RU | ⚠️ | Ядро i18n есть (`i18n/index.js`, `ru.js`/`ru.data.js`, `$t`), линтер проходит зелёным (проверено: `npm run lint:i18n` → ✓). Но: **нет UI-переключателя языка, нет второго языка и нет сохранения выбора** (в `index.js` прямо: «UI-переключателя на этом этапе нет»); даты/числа форматируются жёстко через `toLocale*('ru-RU', …)` (`store/core.js`, `store/settings.js`, `fmtNum` в `store/operator.js`), а не через locale-aware i18n-форматтеры |
|
||||
|
||||
## §12. Ограничения и допущения
|
||||
|
||||
| № | Требование | Статус | Доказательство |
|
||||
|---|---|---|---|
|
||||
| 12.1 | Фронтенд Vue 3 + Vite + Tailwind; единый контракт `/api/cards` + `/api/containers` с этапа 9 | ✅ | `package.json` (vue/vite/tailwind), `api.js`, `CardsEndpoints.cs`, `ContainersEndpoints.cs` |
|
||||
| 12.2 | Данные LeadRadar тестовые — не мигрируются | ✅ | Отдельные миграции Deal; данных-миграций из LeadRadar нет |
|
||||
| 12.3 | Kafka, k8s, биллинг-провайдер, саморегистрация — вне рамок | ✅ | В коде отсутствуют |
|
||||
| 12.4 | 1 Telegram-аккаунт на тенанта; несколько — позже | ✅ | `TenantSession` (1 на тенанта) |
|
||||
|
||||
---
|
||||
|
||||
## Расширенные требования (этапы 8–12)
|
||||
|
||||
| № | Требование | Статус | Доказательство |
|
||||
|---|---|---|---|
|
||||
| E1 | Библиотека готовых промптов по специальностям | ✅ | `ru.data.js` `PROMPT_LIBRARY` (IT/дизайн/недвижимость/стройка/услуги/красота/обучение), `PromptLibraryModal.vue` |
|
||||
| E2 | Категории и поиск в библиотеке | ✅ | `PROMPT_CATEGORIES`, фильтр `query`/`cat` в `PromptLibraryModal.vue` |
|
||||
| E3 | Раздел «Мои промпты» + свой промпт | ✅ | `SettingsService.PatchMyPrompts.cs`, `AiTab.vue` (`addMyPrompt`/`removeMyPrompt`), лимит ≤100 |
|
||||
| E4 | Двухэтапный стоп-лист: стоп-фразы без ИИ, затем ИИ-фильтр с возможностью отключить | ✅ | Этап 1 `IncomingRules` (без ИИ); ИИ-фильтр `FilterSafelyAsync` под `AiFilterEnabled` |
|
||||
| E5 | Исключения внутри колонки | ✅ | `ColumnExclusions.cs` (veto `Exclude`), `BoardRulesDialog.vue` |
|
||||
| E6 | Discovery: поиск/вступление в каналы и группы | ✅ | `DiscoveryWorkerService.Search/Join`, `DiscoveryOps` (telegram-service) |
|
||||
| E7 | Discovery: квоты/интервалы, закрытые группы, темы, список на рассмотрение | ✅ | `DiscoveryBanGuard`, `DiscoveryPacer`, `MarkClosedGroup`, `DiscoveryTopicGroup`, статус `review` |
|
||||
| E8 | «Перечитать каналы»/backfill, пометка прочитанными, мгновенный приём | ✅ | `BackfillService.cs` (10 сообщений, паузы), read-ack; `RealtimeListener.cs` |
|
||||
| E9 | ML отдельным контейнером | ✅ | `src/ml-service/Deal.Ml/Dockerfile` + `compose.dev.yml`/`compose.prod.yml` (`ml-service`, gRPC :5103) |
|
||||
| E10 | ML: обучение на действиях пользователя **и** ИИ | ✅ | Пользователь — `CardsService.Operations.cs` (`PushAsync(…,1.0)`); ИИ — `AiCardLearning.cs`, `CardReclassifier.cs` (`AiPushWeight`) |
|
||||
| E11 | Отдельная настройка проверки ML на сообщении/канале | ⚠️ | Проверка на **сообщении** ✅ (`POST /api/ml/predict`, `MLPanel.vue`); проверка на **канале** ❌ (`/api/ml/candidates` — пустая заглушка, `/api/ml/apply` — 404) |
|
||||
| E12 | Архив/корзина (сроки, возврат, ручная очистка) | ✅ | `StorageTickService.cs`, `CardsService.Operations.cs`, `clear-col`/`DELETE`, `Restore` |
|
||||
| E13 | Напоминания «Отложено» (календарь, отключение) | ✅ | `HoldReminderDialog.vue`, `CardsService.Reminders.cs`, `RemindersEnabled` |
|
||||
| E14 | История карточки под спойлером | ✅ | `CardDrawer.vue` `<details>` «История движения» |
|
||||
| E15 | Контакты квалифицированные (tg/phone/email/linkedin/site) | ✅ | `Parse/ContactsQualifier.cs` (типы `tg/phone/email/linkedin/whatsapp/site`, дедуп, отбой ботов/сервисных ссылок) |
|
||||
| E16 | «Открыть исходник» | ✅ | `CardDrawer.vue` (`sourceUrl`), `ProcessingView.vue` |
|
||||
| E17 | Источник не на карточке (только в деталях) | ✅ | `Card.vue` показывает лишь бейдж «Локальная»/контакты; канал и исходное сообщение — в `CardDrawer.vue` |
|
||||
| E18 | Бюджет: диапазон/вакансия/валюта + конвертация (4 раза в сутки) | ✅ | `CardBudget.cs`, `BudgetNormalizer.cs`, `RatesService.cs` (6 ч = 4/сутки), `ConversionRecomputer.cs` |
|
||||
| E19 | Обязательность суммы (опционально для вакансий) | ✅ | `SettingsKeys.BudgetRequiredHire/BudgetRequiredOrder`, `SkipNoBudgetAsync` |
|
||||
| E20 | Вкладка «Обработка» (очередь + отсев + причины + поиск) | ✅ | `ProcessingView.vue`, `PipelineEndpoints` |
|
||||
| E21 | Возврат из отсева с обучением | ✅ | `PipelineProcessingService.ReturnAsync` (`PushAsync(spam,−1.0)`, `Force`) |
|
||||
| E22 | Оператор-консоль | ✅ | `views/operator/*` (Tenants/Invites/Limits/Audit/Analytics/Health), `router.js` |
|
||||
| E23 | Аналитика токенов | ✅ | `AnalyticsService.cs`, `OperatorAnalyticsEndpoints.cs`, `token_usage_events` |
|
||||
| E24 | Аудит входов/выходов/действий (этап 10) | ✅ | `AuditEvents.cs`, `AuditService.cs`, `AuditSection.vue` |
|
||||
| E25 | i18n (вынос строк) | ⚠️ | Строки вынесены и линтер зелёный, но нет переключателя языка/второго языка/персистентности и locale-форматтеров (см. 11.12) |
|
||||
| E26 | Метрики Prometheus | ✅ | `DealMetricsHosting.cs`, `SharedKernel/Observability/DealMetrics.cs`, `prometheus.yml` (таргеты 5/5) |
|
||||
| E27 | Распределённый rate-limit | ✅ | `RateLimitCounterStore.cs` (Postgres), `StoreBackedFixedWindowRateLimiter.cs`, миграция `RateLimitCounters` |
|
||||
| E28 | reclassify (реальный, этап 12) | ✅ | `CardsEndpoints` `/reclassify` и `/{id}/reclassify`, `CardReclassifier.cs` (локальный фолбэк), `ReclassifyGate.cs`, audit `card_reclassified` |
|
||||
|
||||
---
|
||||
|
||||
## Найденные пропуски/расхождения
|
||||
|
||||
### ❌ Отсутствует
|
||||
|
||||
1. **§8.12 «Внешний вид» (настройки оформления).** В `SettingsView.vue` нет вкладки/раздела внешнего вида;
|
||||
тема одна (тёмная, `style.css` `@theme`). Отдельной настройки «внешний вид» не найдено.
|
||||
|
||||
### ⚠️ Частично
|
||||
|
||||
2. **§8.5 / E11 «проверка ML на канале».** `POST /api/ml/candidates` (`MlEndpoints.cs:131–141`) возвращает
|
||||
`{items: []}` с комментарием «До этапа 6 telegram-данных нет» — устаревшая заглушка; `POST /api/ml/apply`
|
||||
(`MlEndpoints.cs:144–155`) всегда отвечает 404 «Исходное сообщение не найдено». Реального разбора
|
||||
сообщений канала/ручного применения решения ML нет, хотя telegram-данные в системе уже есть
|
||||
(проверка на сообщении — `POST /api/ml/predict` — работает).
|
||||
3. **§5.14 «Глобальные исключения по ключевым словам/технологиям/бюджету/локации».** Глобальных настроек
|
||||
такого исключения в `SettingsKeys` нет: есть только `stopPhrases` (стоп-фразы) и per-column `exclude`
|
||||
(`ColumnExclusions.cs`). Исключения уровня «технология/бюджет/локация» как общий фильтр не найдены.
|
||||
4. **§4.1/§8.1 ключи Telegram.** Хранятся как настройка тенанта `tgKeys` (`TelegramKeysService.cs`) и
|
||||
вводятся в UI тенанта (`TelegramTab.vue`). ТЗ требует, чтобы `api_id`/`api_hash` задавал **оператор
|
||||
глобально** — глобальной операторской настройки/ручки нет.
|
||||
5. **§11.12 / E25 i18n.** Строки вынесены в словари (`i18n/locales/ru.js`, `ru.data.js`), `npm run lint:i18n`
|
||||
проходит. Но отсутствуют: UI-переключатель языка, второй язык, сохранение выбора, «переключение на лету»
|
||||
(в `i18n/index.js` явно сказано «UI-переключателя на этом этапе нет»). Форматирование дат/чисел жёстко
|
||||
`ru-RU` (`store/core.js:232–276`, `store/settings.js:351–406`, `store/operator.js:356`), не через
|
||||
locale-aware i18n-форматтеры.
|
||||
6. **§6.3 состав фильтров колонки.** `ContainerRulesDto` = `Mode/Direction/Keywords/Stack/Grade/Exclude/Budget`.
|
||||
ТЗ перечисляет также «уровень/цена/локация/тип» отдельными опциями — явных групп нет (частично
|
||||
покрываются `direction`/`keywords`).
|
||||
7. **§6.6 «открыть исходник» как быстрое действие карточки.** На `Card.vue` есть комментарий/корзина/контакт,
|
||||
но ссылки «открыть исходник» нет — она доступна только в `CardDrawer.vue` и `ProcessingView.vue`.
|
||||
8. **§10.2 health очередей.** `/api/operator/health` проверяет БД и сервисы ml/ai/telegram, но глубины
|
||||
очередей (пайплайн, MlOutbox) в JSON health не отдаёт — они только в метриках
|
||||
(`DealMetricsCollector.cs` → `/metrics`).
|
||||
9. **§10.5 подозрительная активность.** Специализированного детектора/ленты подозрительной активности по
|
||||
логам безопасности не найдено; есть лишь счётчик `failedLogins` в обзорной аналитике и общие
|
||||
Grafana-дашборды.
|
||||
10. **§11.6 Cloudflare.** В репозитории нет конфигурации/интеграции Cloudflare (edge — Caddy,
|
||||
`deploy/caddy/Caddyfile`). Требование периметра, вне кода приложения.
|
||||
11. **§7.1 «автопрокрутка» очереди.** Реализована как периодическое авто-обновление списка (~2.6 с,
|
||||
`ProcessingView.vue`), а не как буквальная авто-прокрутка. Семантически покрывает требование, но не
|
||||
дословно.
|
||||
|
||||
### Дефекты/легаси, замеченные при проверке (не пункты ТЗ, но влияют на заявленные функции)
|
||||
|
||||
12. **`NotifyTab.vue` — сломан список активных напоминаний.** `const holdReminders = computed(() => state.projectCards.filter(...))`
|
||||
(`settings/NotifyTab.vue:6–8`), при этом `state.projectCards` больше нигде в `src/` не определяется
|
||||
(grep даёт ровно одно совпадение — этот файл). После этапа 9 (`projectCards`/`stage` упразднены) обращение
|
||||
к `state.projectCards.filter` даёт `undefined.filter` → ошибка рендера вкладки «Уведомления».
|
||||
13. **Легаси-артефакты LeadRadar.** В корне остались `docker-compose.yml` (сервисы `app`/`ml`/`minio`
|
||||
старого стека), каталог `backend/` (python `app/`) и `mlservice/` (python). Текущая архитектура — `deploy/compose.*.yml`
|
||||
+ `src/{core,ai,ml,telegram}-service`. Прямого нарушения ТЗ нет, но это риск путаницы (в STATUS.md
|
||||
«судьба legacy `docker-compose.yml`» помечена как открытый вопрос).
|
||||
|
||||
---
|
||||
|
||||
## Чего проверка не покрывает
|
||||
|
||||
- **Живые внешние интеграции без кредов.** Реальный Telegram-вход (`api_id`/`api_hash`/QR) и реальные
|
||||
LLM-вызовы не проверялись (нет кредов; см. STATUS.md, п.5 «нужны живые креды»). Проверяется только
|
||||
наличие кода/контрактов и локальных заглушек.
|
||||
- **Живой контур Docker/k8s, mTLS-рукопожатие, Grafana/Loki/Prometheus.** Проверены конфиги
|
||||
(`compose.*.yml`, `deploy/observability/*`) и код обвязки, но не факт поднятия/скрейпа в этой сессии
|
||||
(сервисы не поднимались).
|
||||
- **Скрипты бэкапа/восстановления и нагрузочные тесты.** Наличие и читаемость проверены (`scripts/backup.sh`,
|
||||
`scripts/restore.sh`, `scripts/loadtest/`), но не выполнялись.
|
||||
- **Корректность чисел в тестах.** Тест-счётчики (STATUS.md: core 1203 и т.п.) не пересчитывались —
|
||||
тесты не запускались (кроме быстрого `lint:i18n`).
|
||||
- **UI-поведение в браузере.** Выводы по фронту основаны на чтении `.vue`/`.js`; реальные клики,
|
||||
drag&drop и рендер не воспроизводились.
|
||||
- **Внешний периметр (Cloudflare, TLS в проде, DNS, egress-контроль).** Вне репозитория.
|
||||
- **Соответствие формальным юридическим требованиям/биллингу** — вне рамок ТЗ (заявлено как «позже»).
|
||||
|
||||
---
|
||||
|
||||
## Обновление (2026-09-10, вечер) — статус после добивки
|
||||
|
||||
Часть найденных ⚠️/❌ закрыта в тот же день (детали — `.superpowers/sdd/deal-stage12-observability-hardening/task-tz-*.md`):
|
||||
|
||||
| Пункт | Было | Стало |
|
||||
|---|---|---|
|
||||
| §8.12 «Внешний вид» | ❌ | ✅ раздел настроек + темы тёмная/светлая/системная (§15 техдока) |
|
||||
| §8/E11 ML-проверка на канале | ⚠️ заглушка | ✅ `MlReviewService` (`/api/ml/candidates|apply`) |
|
||||
| §5.14 глобальные исключения | ⚠️ | ✅ `excludeKeywords/Locations/Types/Budget*` на стоп-этапе |
|
||||
| §6.3 группы фильтров колонки | ⚠️ | ✅ `levels/locations/types/prices` + matchHits |
|
||||
| §6.6 «открыть исходник» на карточке | ⚠️ | ✅ быстрое действие в `Card.vue` |
|
||||
| §10.2 health очередей | ⚠️ | ✅ `queues`/`sessions` в `/api/operator/health` |
|
||||
| §10.5 подозрительная активность | ⚠️ | ✅ `SuspiciousActivityService` + `/api/operator/analytics/suspicious` |
|
||||
|
||||
Остаются требующими владельца/кредов (осознанно): глобальные Telegram-ключи оператора (§4.1/§8.1),
|
||||
переключатель языка (§11.12 — **в бэклоге**, по потребности), живые Telegram/LLM-вызовы, Cloudflare/прод-периметр.
|
||||
Итог после добивки: core-тесты **1245/1245**; фронт build + `lint:i18n` зелёные.
|
||||
|
||||
@@ -1,95 +1,95 @@
|
||||
# Финальная «подбивка» документации «Дейл» (2026-09-11)
|
||||
|
||||
> Дата: 2026-09-11
|
||||
> Периметр: все `docs/**` (актуальные доки — spec/user-guide/technical/api/STATUS; исторические —
|
||||
> `plans/*`, `reviews/*`, `specs/*`, старые `architecture/*`).
|
||||
> Метод: сквозной поиск по проблемным терминам (`Boards`, `ProjectCards`, `Deal.Modules.Projects`,
|
||||
> `ProjectStages`, корневой `docker-compose.yml`, `DEAL_DEMO`, демо-эндпоинты, `l_`/`pr_`, `app_settings`,
|
||||
> «лид» как сущность, старые порты/пути/счётчики тестов) + чтение актуальных доков и сверка с кодом
|
||||
> (`src/**`, `deploy/compose.*.yml`, `scripts/dev-smoke.sh`, `deploy/observability/grafana/dashboards/`).
|
||||
> Докер не поднимался, тесты не перезапускались. Предшествующий аудит — `2026-09-10-docs-audit.md`
|
||||
> (30 расхождений, уже помечен как исторический).
|
||||
|
||||
## Сводка
|
||||
|
||||
- Найдено новых расхождений: **9** (по таблице ниже).
|
||||
- Исправлено в актуальных доках: **9**.
|
||||
- Добавлено исторических пометок: **20** файлов.
|
||||
- Переписывание содержания исторических артефактов не выполнялось (по правилам задачи).
|
||||
|
||||
## Расхождения (файл:строка → в доке → реальность → действие)
|
||||
|
||||
| # | Файл:строка | В доке | Реальность | Действие |
|
||||
|---|---|---|---|---|
|
||||
| 1 | `docs/superpowers/STATUS.md` ~L4 | «Все этапы **0–10** выполнены (100%)» | таблица этапов — **0–12**, «Итого 0–12 = 100%» (строка ниже) | ✅ исправлено на 0–12 |
|
||||
| 2 | `docs/superpowers/STATUS.md` ~L25 | этап 10 — «**ELK**-дашборды» | стек — Loki + promtail + Grafana (`deploy/observability/grafana/dashboards/Deal-*.json`); Elasticsearch/Kibana нет | ✅ «Grafana/Loki-дашборды» |
|
||||
| 3 | `docs/superpowers/STATUS.md` ~L54 | «Настройки (ключи **AI/Telegram** enc:, промпты, валюты)» у тенанта | ключи Telegram — глобально у оператора (`public.global_settings`); у тенанта только подключение аккаунта | ✅ «ключи AI enc: …; Telegram-ключи — глобально у оператора» |
|
||||
| 4 | `docs/superpowers/STATUS.md` ~L134 | «судьба legacy `docker-compose.yml`» (открытый вопрос) | файл перенесён в `archive/leadradar-legacy/` (2026-09-10) | ✅ «перенесён в `archive/leadradar-legacy/`» |
|
||||
| 5 | `docs/superpowers/STATUS.md` ~L141 | «Core-тесты **1245/1245**» | актуально **1275/1275** (в том же разделе ниже уже 1275) | ✅ исправлено на 1275/1275 |
|
||||
| 6 | `docs/superpowers/STATUS.md` ~L167 | «реестр id-стадий `ProjectStages`» | с этапа 9 каталог — `CardsDefaultContainers` (`Deal.Modules.Cards/Application/CardsDefaultContainers.cs`) | ✅ аннотировано «(с этапа 9 — `CardsDefaultContainers`)» |
|
||||
| 7 | `docs/superpowers/STATUS.md` ~L7, ~L94 | «dev-smoke **12/12**» (в двух местах) | `scripts/dev-smoke.sh` выполняет **14** проверок (config + 6 контейнеров + login/status/containers/create/list/trash/ML-флашер); таблица этапа 9 уже фиксирует `PASS=14` | ✅ исправлено на 14/14 |
|
||||
| 8 | `docs/technical/Техническая-документация-Дейл.md` §8 ~L319 | `dev-smoke.sh`: «… → `/api/tg/status` → **simulate-lead** → флашер MlOutbox …» | скрипт: `/api/tg/status` → `POST /api/cards` → trash → флашер MlOutbox | ✅ заменено на `POST /api/cards` → trash |
|
||||
| 9 | `docs/user-guide/Инструкция-пользователя-Дейл.md` ~L6, L32-34, L41, L262 | «dev/демо-окружение», «демо-пространство с входом `admin`/`admin`» | демо удалено; dev-seed создаёт bootstrap-тенанта `Default` (env `DEAL_BOOTSTRAP_LOGIN/PASSWORD`, дефолт `admin`/`admin`) | ✅ «Dev-окружение», «bootstrap-пространство `Default`» |
|
||||
|
||||
## Добавленные исторические пометки
|
||||
|
||||
Единая шапка: `> Исторический документ этапа N. Актуальное состояние — docs/superpowers/STATUS.md и docs/technical/Техническая-документация-Дейл.md.`
|
||||
|
||||
Планы (`docs/superpowers/plans/`, 15 файлов):
|
||||
`2026-09-04-channel-discovery.md` (план Discovery прототипа LeadRadar),
|
||||
`2026-09-05-deal-roadmap.md` (roadmap этапов 0–7),
|
||||
`2026-09-05-deal-scaffold.md` (этап 0),
|
||||
`2026-09-05-deal-stage1-tenancy.md` … `2026-09-05-deal-stage7-saas.md` (этапы 1–7),
|
||||
`2026-09-09-deal-stage9-unified-card.md` (этап 9),
|
||||
`2026-09-10-deal-stage10-operator-analytics.md` (этап 10),
|
||||
`2026-09-10-deal-stage11-i18n.md` (этап 11),
|
||||
`2026-09-10-deal-stage12-observability-hardening.md` (этап 12).
|
||||
|
||||
Ревью (`docs/superpowers/reviews/`):
|
||||
`2026-09-08-code-quality-review.md` (этап 8),
|
||||
`2026-09-10-tz-compliance-audit.md` (аудит соответствия ТЗ),
|
||||
`2026-09-10-docs-audit.md` (аудит документации; добавлена ссылка на текущий отчёт).
|
||||
|
||||
Специи/архитектура:
|
||||
`docs/superpowers/specs/2026-09-04-channel-discovery-design.md` (дизайн Discovery прототипа),
|
||||
`docs/architecture/2026-09-05-deal-architecture-design.md` (архдизайн-черновик),
|
||||
`docs/architecture/2026-09-09-unified-card.md` (дизайн единой карточки, этап 9).
|
||||
|
||||
Не тронуты по существу (актуальны): `docs/architecture/2026-09-10-unified-api-contract.md`,
|
||||
`docs/architecture/2026-09-10-operator-analytics-contract.md`.
|
||||
|
||||
## Проверено и сходится
|
||||
|
||||
- **Единый API**: `/api/cards` + `/api/containers`; `api-map` §1/§3.5 корректно фиксирует удаление
|
||||
`/api/leads|projects|boards|columns` и переименование `new_lead → new_card`; ссылки на «бывшие» домены —
|
||||
в контексте «удалено», а не как действующие.
|
||||
- **Ключи Telegram**: spec §4.1/§8, user-guide §3, api-map §6, technical §13.7/§13.10 — везде у оператора
|
||||
(`/api/operator/settings/telegram-keys`, `public.global_settings`).
|
||||
- **Оператор-консоль/активация**: `#/operator`, `#/join?code=…` — spec §10, user-guide §11, technical §13.10,
|
||||
api-map — совпадают.
|
||||
- **Порты**: core 5080/5082, telegram 5101, ai 5102, ml 5103, metrics 9464, postgres 5433, minio 9000/9001,
|
||||
grafana 3001, prometheus 9090 — совпадают между spec/user-guide/technical/api и compose-файлами.
|
||||
- **Core-тесты**: 1275 (technical §13.6/§16, STATUS таблица/итоги) — противоречий в актуальных доках нет.
|
||||
- **Префиксы id**: `c_` (единый) — api-map §4.1, technical §11/§12/§8; `l_`/`pr_` в актуальных доках отсутствуют
|
||||
(остались только в помеченных исторических разделах и внешних исторических артефактах).
|
||||
- **`app_settings`**: в актуальных доках нет; актуальная таблица — `global_settings` (`public`).
|
||||
- **Исторические артефакты**: `Boards`/`ProjectCards`/`Deal.Modules.Projects`/`ProjectStages`/`DEAL_DEMO`/
|
||||
демо-ручки/`docker-compose.yml` встречаются только в документах, получивших историческую пометку.
|
||||
|
||||
## Осталось спорным / намеренно не тронуто
|
||||
|
||||
1. **Ссылка из spec на исторический архдизайн.** `docs/spec/ТЗ-дейл-новая-архитектура.md` (шапка)
|
||||
указывает среди связанных `docs/architecture/2026-09-05-deal-architecture-design.md` — документ теперь
|
||||
помечен историческим. Формально не ошибка (файл существует, помечен), но при следующей редакции ссылку,
|
||||
возможно, стоит заменить на `2026-09-10-unified-api-contract.md`.
|
||||
2. **`tgKeys` в historical §13.4b технического дока** (`GET /api/settings` перечисляет `tgKeys`): раздел
|
||||
помечен историческим (§13 шапка + заметка §13.4a о переносе ключей к оператору). По правилам задачи
|
||||
содержание исторических разделов не переписывалось.
|
||||
3. **Счётчики тестов сервисов** (telegram 125, ai 52, ml 38): не перепроверялись кодом/прогоном
|
||||
(запрет на долгие процессы); в актуальных доках они не противоречат друг другу.
|
||||
4. **«Live SaaS 15/15»** — цифра из исторических приёмок, независимо не подтверждалась.
|
||||
5. **Число операторских ручек (25)** в api-map §5 — подсчёт по `Endpoints/Operator*` + `/api/join`;
|
||||
группировка может отличаться от авторской (ранее было 21). Не перепроверялось.
|
||||
6. **Исторический журнал §11 техдока** (этапы 1–7) и §13.4c/4d/4e намеренно сохраняют легаси-термины
|
||||
под пометками; сведение их в ссылки на §3 — задача следующей редакции, а не этой подбивки.
|
||||
7. **`docs/architecture/2026-09-10-*`** (контракты) по условию задачи не редактировались; они актуальны.
|
||||
# Финальная «подбивка» документации «Дейл» (2026-09-11)
|
||||
|
||||
> Дата: 2026-09-11
|
||||
> Периметр: все `docs/**` (актуальные доки — spec/user-guide/technical/api/STATUS; исторические —
|
||||
> `plans/*`, `reviews/*`, `specs/*`, старые `architecture/*`).
|
||||
> Метод: сквозной поиск по проблемным терминам (`Boards`, `ProjectCards`, `Deal.Modules.Projects`,
|
||||
> `ProjectStages`, корневой `docker-compose.yml`, `DEAL_DEMO`, демо-эндпоинты, `l_`/`pr_`, `app_settings`,
|
||||
> «лид» как сущность, старые порты/пути/счётчики тестов) + чтение актуальных доков и сверка с кодом
|
||||
> (`src/**`, `deploy/compose.*.yml`, `scripts/dev-smoke.sh`, `deploy/observability/grafana/dashboards/`).
|
||||
> Докер не поднимался, тесты не перезапускались. Предшествующий аудит — `2026-09-10-docs-audit.md`
|
||||
> (30 расхождений, уже помечен как исторический).
|
||||
|
||||
## Сводка
|
||||
|
||||
- Найдено новых расхождений: **9** (по таблице ниже).
|
||||
- Исправлено в актуальных доках: **9**.
|
||||
- Добавлено исторических пометок: **20** файлов.
|
||||
- Переписывание содержания исторических артефактов не выполнялось (по правилам задачи).
|
||||
|
||||
## Расхождения (файл:строка → в доке → реальность → действие)
|
||||
|
||||
| # | Файл:строка | В доке | Реальность | Действие |
|
||||
|---|---|---|---|---|
|
||||
| 1 | `docs/superpowers/STATUS.md` ~L4 | «Все этапы **0–10** выполнены (100%)» | таблица этапов — **0–12**, «Итого 0–12 = 100%» (строка ниже) | ✅ исправлено на 0–12 |
|
||||
| 2 | `docs/superpowers/STATUS.md` ~L25 | этап 10 — «**ELK**-дашборды» | стек — Loki + promtail + Grafana (`deploy/observability/grafana/dashboards/Deal-*.json`); Elasticsearch/Kibana нет | ✅ «Grafana/Loki-дашборды» |
|
||||
| 3 | `docs/superpowers/STATUS.md` ~L54 | «Настройки (ключи **AI/Telegram** enc:, промпты, валюты)» у тенанта | ключи Telegram — глобально у оператора (`public.global_settings`); у тенанта только подключение аккаунта | ✅ «ключи AI enc: …; Telegram-ключи — глобально у оператора» |
|
||||
| 4 | `docs/superpowers/STATUS.md` ~L134 | «судьба legacy `docker-compose.yml`» (открытый вопрос) | файл перенесён в `archive/leadradar-legacy/` (2026-09-10) | ✅ «перенесён в `archive/leadradar-legacy/`» |
|
||||
| 5 | `docs/superpowers/STATUS.md` ~L141 | «Core-тесты **1245/1245**» | актуально **1275/1275** (в том же разделе ниже уже 1275) | ✅ исправлено на 1275/1275 |
|
||||
| 6 | `docs/superpowers/STATUS.md` ~L167 | «реестр id-стадий `ProjectStages`» | с этапа 9 каталог — `CardsDefaultContainers` (`Deal.Modules.Cards/Application/CardsDefaultContainers.cs`) | ✅ аннотировано «(с этапа 9 — `CardsDefaultContainers`)» |
|
||||
| 7 | `docs/superpowers/STATUS.md` ~L7, ~L94 | «dev-smoke **12/12**» (в двух местах) | `scripts/dev-smoke.sh` выполняет **14** проверок (config + 6 контейнеров + login/status/containers/create/list/trash/ML-флашер); таблица этапа 9 уже фиксирует `PASS=14` | ✅ исправлено на 14/14 |
|
||||
| 8 | `docs/technical/Техническая-документация-Дейл.md` §8 ~L319 | `dev-smoke.sh`: «… → `/api/tg/status` → **simulate-lead** → флашер MlOutbox …» | скрипт: `/api/tg/status` → `POST /api/cards` → trash → флашер MlOutbox | ✅ заменено на `POST /api/cards` → trash |
|
||||
| 9 | `docs/user-guide/Инструкция-пользователя-Дейл.md` ~L6, L32-34, L41, L262 | «dev/демо-окружение», «демо-пространство с входом `admin`/`admin`» | демо удалено; dev-seed создаёт bootstrap-тенанта `Default` (env `DEAL_BOOTSTRAP_LOGIN/PASSWORD`, дефолт `admin`/`admin`) | ✅ «Dev-окружение», «bootstrap-пространство `Default`» |
|
||||
|
||||
## Добавленные исторические пометки
|
||||
|
||||
Единая шапка: `> Исторический документ этапа N. Актуальное состояние — docs/superpowers/STATUS.md и docs/technical/Техническая-документация-Дейл.md.`
|
||||
|
||||
Планы (`docs/superpowers/plans/`, 15 файлов):
|
||||
`2026-09-04-channel-discovery.md` (план Discovery прототипа LeadRadar),
|
||||
`2026-09-05-deal-roadmap.md` (roadmap этапов 0–7),
|
||||
`2026-09-05-deal-scaffold.md` (этап 0),
|
||||
`2026-09-05-deal-stage1-tenancy.md` … `2026-09-05-deal-stage7-saas.md` (этапы 1–7),
|
||||
`2026-09-09-deal-stage9-unified-card.md` (этап 9),
|
||||
`2026-09-10-deal-stage10-operator-analytics.md` (этап 10),
|
||||
`2026-09-10-deal-stage11-i18n.md` (этап 11),
|
||||
`2026-09-10-deal-stage12-observability-hardening.md` (этап 12).
|
||||
|
||||
Ревью (`docs/superpowers/reviews/`):
|
||||
`2026-09-08-code-quality-review.md` (этап 8),
|
||||
`2026-09-10-tz-compliance-audit.md` (аудит соответствия ТЗ),
|
||||
`2026-09-10-docs-audit.md` (аудит документации; добавлена ссылка на текущий отчёт).
|
||||
|
||||
Специи/архитектура:
|
||||
`docs/superpowers/specs/2026-09-04-channel-discovery-design.md` (дизайн Discovery прототипа),
|
||||
`docs/architecture/2026-09-05-deal-architecture-design.md` (архдизайн-черновик),
|
||||
`docs/architecture/2026-09-09-unified-card.md` (дизайн единой карточки, этап 9).
|
||||
|
||||
Не тронуты по существу (актуальны): `docs/architecture/2026-09-10-unified-api-contract.md`,
|
||||
`docs/architecture/2026-09-10-operator-analytics-contract.md`.
|
||||
|
||||
## Проверено и сходится
|
||||
|
||||
- **Единый API**: `/api/cards` + `/api/containers`; `api-map` §1/§3.5 корректно фиксирует удаление
|
||||
`/api/leads|projects|boards|columns` и переименование `new_lead → new_card`; ссылки на «бывшие» домены —
|
||||
в контексте «удалено», а не как действующие.
|
||||
- **Ключи Telegram**: spec §4.1/§8, user-guide §3, api-map §6, technical §13.7/§13.10 — везде у оператора
|
||||
(`/api/operator/settings/telegram-keys`, `public.global_settings`).
|
||||
- **Оператор-консоль/активация**: `#/operator`, `#/join?code=…` — spec §10, user-guide §11, technical §13.10,
|
||||
api-map — совпадают.
|
||||
- **Порты**: core 5080/5082, telegram 5101, ai 5102, ml 5103, metrics 9464, postgres 5433, minio 9000/9001,
|
||||
grafana 3001, prometheus 9090 — совпадают между spec/user-guide/technical/api и compose-файлами.
|
||||
- **Core-тесты**: 1275 (technical §13.6/§16, STATUS таблица/итоги) — противоречий в актуальных доках нет.
|
||||
- **Префиксы id**: `c_` (единый) — api-map §4.1, technical §11/§12/§8; `l_`/`pr_` в актуальных доках отсутствуют
|
||||
(остались только в помеченных исторических разделах и внешних исторических артефактах).
|
||||
- **`app_settings`**: в актуальных доках нет; актуальная таблица — `global_settings` (`public`).
|
||||
- **Исторические артефакты**: `Boards`/`ProjectCards`/`Deal.Modules.Projects`/`ProjectStages`/`DEAL_DEMO`/
|
||||
демо-ручки/`docker-compose.yml` встречаются только в документах, получивших историческую пометку.
|
||||
|
||||
## Осталось спорным / намеренно не тронуто
|
||||
|
||||
1. **Ссылка из spec на исторический архдизайн.** `docs/spec/ТЗ-дейл-новая-архитектура.md` (шапка)
|
||||
указывает среди связанных `docs/architecture/2026-09-05-deal-architecture-design.md` — документ теперь
|
||||
помечен историческим. Формально не ошибка (файл существует, помечен), но при следующей редакции ссылку,
|
||||
возможно, стоит заменить на `2026-09-10-unified-api-contract.md`.
|
||||
2. **`tgKeys` в historical §13.4b технического дока** (`GET /api/settings` перечисляет `tgKeys`): раздел
|
||||
помечен историческим (§13 шапка + заметка §13.4a о переносе ключей к оператору). По правилам задачи
|
||||
содержание исторических разделов не переписывалось.
|
||||
3. **Счётчики тестов сервисов** (telegram 125, ai 52, ml 38): не перепроверялись кодом/прогоном
|
||||
(запрет на долгие процессы); в актуальных доках они не противоречат друг другу.
|
||||
4. **«Live SaaS 15/15»** — цифра из исторических приёмок, независимо не подтверждалась.
|
||||
5. **Число операторских ручек (25)** в api-map §5 — подсчёт по `Endpoints/Operator*` + `/api/join`;
|
||||
группировка может отличаться от авторской (ранее было 21). Не перепроверялось.
|
||||
6. **Исторический журнал §11 техдока** (этапы 1–7) и §13.4c/4d/4e намеренно сохраняют легаси-термины
|
||||
под пометками; сведение их в ссылки на §3 — задача следующей редакции, а не этой подбивки.
|
||||
7. **`docs/architecture/2026-09-10-*`** (контракты) по условию задачи не редактировались; они актуальны.
|
||||
|
||||
@@ -1,212 +1,212 @@
|
||||
# Поиск и подключение каналов (Discovery) — дизайн
|
||||
|
||||
> Исторический документ (дизайн Discovery прототипа LeadRadar, 2026-09-04). Актуальное состояние — `docs/superpowers/STATUS.md` и `docs/technical/Техническая-документация-Дейл.md`.
|
||||
|
||||
Дата: 2026-09-04
|
||||
Статус: согласован с пользователем (правки от 2026-09-04 учтены)
|
||||
|
||||
## 1. Цель
|
||||
|
||||
Пользователь даёт системе «задание»: найти Telegram-каналы и группы, в которых мы ещё
|
||||
**не состоим**, по описанию цели (например, «вакансии и фриланс для разработки») и
|
||||
подключить их к мониторингу. Система сама ищет кандидатов, оценивает их (по метаданным,
|
||||
языку и содержанию сообщений) и показывает человеку список «на рассмотрение»; человек
|
||||
решает — вступить и мониторить или отклонить. Возможен режим авто-вступления в рамках
|
||||
суточных квот и с паузами против бана.
|
||||
|
||||
Ключевое правило: **источники, в которых мы уже состоим (вступили/мониторим), исключаются
|
||||
сразу и безусловно — независимо от запроса, ключей и настроек задачи.** Это глобальное
|
||||
правило системы: действует на всех этапах (поиск → оценка → вступление) и для всех задач.
|
||||
|
||||
## 2. Ограничения Telegram API (факты, на которых строится дизайн)
|
||||
|
||||
1. Глобального «поиска по критериям» в API нет. `contacts.search(q)` возвращает
|
||||
публичные каналы/группы/боты по **имени/username/запросу** — без фильтров по
|
||||
участникам, языку и содержимому. Всю дальнейшую фильтрацию делаем сами.
|
||||
2. Число участников/описание — через `channels.getFullChannel`. Для публичных каналов
|
||||
доступно без вступления; для групп часто доступно только членам.
|
||||
3. Чтение истории без вступления: публичные **каналы** — обычно можно; публичные
|
||||
**группы** — только если история открыта; иначе — только членам.
|
||||
4. Массовый поиск/чтение/вступления с юзер-аккаунта ограничены эмпирически — нужны
|
||||
квоты, паузы и обработка `FloodWaitError`.
|
||||
|
||||
## 3. Понятия
|
||||
|
||||
- **Задача (task)** — конфиг поиска: описание цели, ключи, фильтры, план, режим
|
||||
авто-вступления, статус/счётчики. Задач может быть несколько.
|
||||
- **Кандидат (candidate)** — найденный источник (канал/группа, для форумов — оценка по
|
||||
темам). Проходит стадии: `new → evaluated → review → joined | rejected`.
|
||||
- **Метки кандидата** — человекочитаемые пометки: «закрытая группа/канал», «форум»,
|
||||
«не прочитано», «участники не подтверждены», «язык не подтверждён», «есть проходные
|
||||
темы».
|
||||
- **Чёрный список** — источники, отклонённые пользователем; поиск их больше не
|
||||
возвращает (снимается вручную).
|
||||
- **BanGuard** — единый менеджер квот и пауз для всех действий discovery
|
||||
(search/read/join/leave), общий для задач.
|
||||
|
||||
## 4. Задача: конфигурация и правила создания
|
||||
|
||||
Поля задачи:
|
||||
|
||||
| Поле | Назначение | По умолчанию |
|
||||
| --- | --- | --- |
|
||||
| `name` | название задачи | — |
|
||||
| `description` | описание цели (что ищем) | — |
|
||||
| `keywords` | поисковые ключи (генерирует ИИ, редактируются перед стартом) | [] |
|
||||
| `minSubscribers` | минимум участников (0 = не важно) | 0 |
|
||||
| `lang` | язык источников (`ru` / `any`) | `ru` |
|
||||
| `threshold` | доля подходящих сообщений, % | 40 |
|
||||
| `sampleSize` | сколько сообщений смотреть при оценке | 10 |
|
||||
| `planJoins` | план вступлений N | 1..50 |
|
||||
| `autoJoin` | авто-вступление подходящих | false |
|
||||
| `status` | `draft → running → paused → done | failed` | draft |
|
||||
|
||||
Правила создания:
|
||||
|
||||
- **Бюджет планов:** сумма `planJoins` всех задач в статусе не `done/failed` + `planJoins`
|
||||
новой ≤ суточного лимита вступлений (по умолчанию 50). Задача с планом 50 не даёт
|
||||
создать другую; план 25 оставляет максимум 25.
|
||||
- Запуск возможен только после генерации/подтверждения ключей.
|
||||
- При редактировании активной задачи план нельзя увеличить сверх свободного бюджета.
|
||||
|
||||
## 5. Пайплайн поиска (каскад фильтров)
|
||||
|
||||
Выполняется фоновым воркером задачи строго через BanGuard (по одному действию, с паузами).
|
||||
|
||||
Для каждого кандидата фильтры идут **по нарастающей стоимости**; при первом «нет»
|
||||
источник пропускается и берётся следующий:
|
||||
|
||||
1. **Поиск** — `contacts.search` по каждому ключу (с паузами). Кандидаты
|
||||
дедуплицируются по `dialog_id`/username.
|
||||
2. **«Мы не состоим» — глобальный фильтр, применяется сразу и безусловно:** как только
|
||||
источник найден (независимо от запроса/ключей), он отбрасывается, если уже есть
|
||||
в `dialogs` (вступили/мониторим), в чёрном списке или уже обрабатывается/вступил/ждёт
|
||||
рассмотрения в другой задаче (глобальная дедупликация кандидатов). Остальные фильтры
|
||||
(участники/язык/контент) применяются уже после этого. Проверка повторяется
|
||||
непосредственно перед вступлением (между оценкой и join'ом кандидат мог быть добавлен
|
||||
вручную).
|
||||
3. **Число участников** — если `minSubscribers` задано:
|
||||
- значение получено и меньше минимума → пропуск;
|
||||
- значение получить не удалось → **не пропускаем**, ставим метку «участники не
|
||||
подтверждены».
|
||||
4. **Язык** — если `lang=ru`: по выборке сообщений эвристикой кириллицы (без ИИ);
|
||||
не удалось прочитать → метка «язык не подтверждён» (не пропуск).
|
||||
5. **Содержимое** — оценка выборки сообщений (см. §6).
|
||||
|
||||
Пометки «не подтверждено» — не ошибка, а сигнал человеку на экране рассмотрения.
|
||||
|
||||
## 6. Оценка содержимого (по темам, для форумов)
|
||||
|
||||
- **Что считается «подходящим сообщением»:** сообщение проходит те же правила, что в
|
||||
основной системе (этап 1 → ML → ИИ), но **профиль оценки = профиль задачи**
|
||||
(описание + ключи задачи), а не глобальные настройки дашборда. Оценка ничего не
|
||||
создаёт: ни карточек, ни очереди, ни обучения ML.
|
||||
- Если ИИ выключен — оценка локальным разбором/ML.
|
||||
- **Каналы:** читаем до `sampleSize` последних сообщений; доля подходящих ≥ `threshold`
|
||||
→ в «на рассмотрение».
|
||||
- **Открытые группы:** то же; чтение не удалось → «на рассмотрение» с меткой
|
||||
«открытая группа, не прочитана».
|
||||
- **Закрытые группы** (нашлись по ключам, история скрыта): сразу «на рассмотрение» с
|
||||
меткой «закрытая группа/канал» (+ метки неподтверждённых фильтров). Пользователь
|
||||
вступает сам.
|
||||
- **Форумы (группы с темами):** группа раскладывается по темам (`reply_to_top_id`):
|
||||
читаем выборку по активным темам, оценка считается **по темам** («тема: подходит
|
||||
X из N»). Группа подходящая, если есть ≥1 проходная тема. В превью — список тем с
|
||||
пометками проходная/нет. Имена тем, если API не отдаёт без членства, подставляем
|
||||
сниппетом первого сообщения темы.
|
||||
- Порог «40%» применяется к сообщениям темы/канала; если в выборке меньше 3
|
||||
содержательных сообщений — кандидат идёт «на рассмотрение» с меткой «мало сообщений».
|
||||
|
||||
## 7. «На рассмотрение» и действия человека
|
||||
|
||||
Экран по задаче содержит списки: **В обработке / На рассмотрении / Вступили /
|
||||
Отклонены**, плюс история.
|
||||
|
||||
Кандидат на рассмотрении показывает: тип (канал/группа/форум), число участников,
|
||||
метки, долю «подходит X из N» и **почему подошло** (перечень подходящих сообщений/тем
|
||||
с причинами — как блок «попала по фильтру» в карточках), превью сообщений (для
|
||||
форумов — по темам).
|
||||
|
||||
Действия:
|
||||
|
||||
- **«Вступить и мониторить»** — `channels.joinChannel` (по username), добавление в
|
||||
`dialogs` с `monitor=1`, backfill последних ~10 сообщений. Ручной клик — **вне квот**.
|
||||
После вступления источник автоматически попадает под правило «мы состоим» и из
|
||||
поиска исключается.
|
||||
- **«Отклонить»** — источник в чёрный список (исключается из поиска во всех задачах).
|
||||
Если для оценки пришлось вступать — выходим (`channels.leaveChannel`) в рамках квот.
|
||||
Чёрный список редактируется вручную (можно снять).
|
||||
- **Закрытые группы:** вместо авто-вступления — кнопка-ссылка `t.me/<username>`; система
|
||||
замечает вступление при синхронизации диалогов и предлагает добавить источник в
|
||||
мониторинг (метка «вступили, добавить в мониторинг?»).
|
||||
|
||||
## 8. Авто-вступление, квоты и анти-бан (BanGuard)
|
||||
|
||||
- Суточный лимит вступлений — **50** (настройка), общий для всех задач, считаются только
|
||||
автоматические вступления. Ручные — без ограничений.
|
||||
- Авто-вступление включается на задачу (`autoJoin`). Подходящие кандидаты вступают сами.
|
||||
- Интервалы между автоматическими вступлениями: **случайно 50–70 секунд**; по одному
|
||||
действию, без параллелей. Поиск и чтение — мягкие паузы (единицы секунд + джиттер,
|
||||
переиспользуем значения анти-бана из telegram.py).
|
||||
- Задача «выполнена» при достижении плана вступлений. Если за сутки упёрлись в общий
|
||||
бюджет — авто-режим продолжает на следующий день (новый суточный бюджет).
|
||||
- `FloodWaitError` → пауза по секундам из ответа + запас; авто-вступления останавливаются
|
||||
до следующего дня при флуде. Общий «стоп-кран» — пауза всего discovery.
|
||||
- Все квоты/интервалы — настройки в UI.
|
||||
|
||||
## 9. Хранилище
|
||||
|
||||
| Таблица | Назначение / ключевые поля |
|
||||
| --- | --- |
|
||||
| `disc_tasks` | задачи: name, description, keywords(JSON), min_subscribers, lang, threshold, sample_size, plan_joins, auto_join, status, counters (found/evaluated/joined/rejected), created/updated |
|
||||
| `disc_candidates` | dialog_id/username/name/kind(channel|group|forum)/hue, participants, lang_ru, join_failures, marks(JSON), topics(JSON: {topicId,title,fitCount,total,fitRatio,passed}), fit_ratio, status(new/review/joined/rejected), task_id, times |
|
||||
| `disc_blacklist` | dialog_id, name, reason, created_at |
|
||||
| `disc_log` | история задачи: task_id, event(search/evaluate/join/leave/flood/error/review/blacklist), text, created_at |
|
||||
|
||||
Дубли кандидатов не создаются; источник, попавший в другую задачу или `dialogs`,
|
||||
из поиска исключается (правило «мы состоим» — глобальное).
|
||||
|
||||
Реализация: у кандидата нет транзитного статуса `evaluated` (счётчик оценённых — на
|
||||
задаче); `join_failures` — неудачные авто-вступления подряд, после 3 кандидат удаляется.
|
||||
|
||||
## 10. API
|
||||
|
||||
- `GET/POST/PATCH/DELETE /api/discovery/tasks` (создание с валидацией бюджета планов),
|
||||
`POST /api/discovery/tasks/{id}/start|pause`
|
||||
- `POST /api/discovery/tasks/{id}/generate-keywords` — ИИ генерирует ключи по описанию
|
||||
- `GET /api/discovery/tasks/{id}/candidates?status=review|joined|rejected`
|
||||
- `POST /api/discovery/candidates/{id}/join` (вступить и мониторить), `.../reject`
|
||||
- `GET /api/discovery/blacklist`, `DELETE /api/discovery/blacklist/{dialog_id}`
|
||||
- `GET /api/discovery/tasks/{id}/log`
|
||||
|
||||
## 11. UI
|
||||
|
||||
Подвкладка **«Поиск»** на экране «Каналы»:
|
||||
- список задач (статус, прогресс, план/вступили, авто-режим) + «Новая задача»;
|
||||
- мастер задачи: описание → «Сгенерировать ключи ИИ» → редактирование ключей →
|
||||
фильтры/план/авто-режим → запуск;
|
||||
- по задаче: статус-лента (поиск → оценка → вступление), вкладки «В обработке /
|
||||
На рассмотрении / Вступили / Отклонены», история;
|
||||
- кандидат на рассмотрении раскрывается с превью и действиями; форум — по темам;
|
||||
- настройки квот (лимит/интервалы) — в том же экране или «Настройки → Telegram».
|
||||
|
||||
## 12. Интеграция с существующим кодом
|
||||
|
||||
- Фоновый воркер discovery — отдельный цикл в `main.py` (как `_pipeline_loop`),
|
||||
сервис `app/services/discovery.py`, Telegram-действия — методы `TelegramManager`
|
||||
(поиск/join/leave/чтение) с общим pacing.
|
||||
- Переиспользуем: `rules/stage1/ML/AI` для оценки сообщений (новый лёгкий вызов с
|
||||
профилем задачи, без записи карточек), список `dialogs` для фильтра «мы состоим»,
|
||||
синхронизацию диалогов для авто-добавления закрытых групп.
|
||||
- К основному пайплайну карточек, ML и ТЗ-логике не прикасаемся.
|
||||
|
||||
## 13. Вне рамок (сейчас)
|
||||
|
||||
- Агрегаторы-каталоги как источник кандидатов.
|
||||
- «Похожие каналы» (`getChannelRecommendations`) от наших подписок — отдельная опция позже.
|
||||
- Авто-вступление в закрытые группы по инвайт-ссылкам (глобальным поиском они не находятся).
|
||||
|
||||
## 14. Значения по умолчанию (настраиваются в UI)
|
||||
|
||||
суточный лимит вступлений = 50; интервал авто-вступлений = 50–70 с; выборка = 10
|
||||
сообщений; порог = 40%; мин. содержательных сообщений для оценки = 3; язык = ru.
|
||||
# Поиск и подключение каналов (Discovery) — дизайн
|
||||
|
||||
> Исторический документ (дизайн Discovery прототипа LeadRadar, 2026-09-04). Актуальное состояние — `docs/superpowers/STATUS.md` и `docs/technical/Техническая-документация-Дейл.md`.
|
||||
|
||||
Дата: 2026-09-04
|
||||
Статус: согласован с пользователем (правки от 2026-09-04 учтены)
|
||||
|
||||
## 1. Цель
|
||||
|
||||
Пользователь даёт системе «задание»: найти Telegram-каналы и группы, в которых мы ещё
|
||||
**не состоим**, по описанию цели (например, «вакансии и фриланс для разработки») и
|
||||
подключить их к мониторингу. Система сама ищет кандидатов, оценивает их (по метаданным,
|
||||
языку и содержанию сообщений) и показывает человеку список «на рассмотрение»; человек
|
||||
решает — вступить и мониторить или отклонить. Возможен режим авто-вступления в рамках
|
||||
суточных квот и с паузами против бана.
|
||||
|
||||
Ключевое правило: **источники, в которых мы уже состоим (вступили/мониторим), исключаются
|
||||
сразу и безусловно — независимо от запроса, ключей и настроек задачи.** Это глобальное
|
||||
правило системы: действует на всех этапах (поиск → оценка → вступление) и для всех задач.
|
||||
|
||||
## 2. Ограничения Telegram API (факты, на которых строится дизайн)
|
||||
|
||||
1. Глобального «поиска по критериям» в API нет. `contacts.search(q)` возвращает
|
||||
публичные каналы/группы/боты по **имени/username/запросу** — без фильтров по
|
||||
участникам, языку и содержимому. Всю дальнейшую фильтрацию делаем сами.
|
||||
2. Число участников/описание — через `channels.getFullChannel`. Для публичных каналов
|
||||
доступно без вступления; для групп часто доступно только членам.
|
||||
3. Чтение истории без вступления: публичные **каналы** — обычно можно; публичные
|
||||
**группы** — только если история открыта; иначе — только членам.
|
||||
4. Массовый поиск/чтение/вступления с юзер-аккаунта ограничены эмпирически — нужны
|
||||
квоты, паузы и обработка `FloodWaitError`.
|
||||
|
||||
## 3. Понятия
|
||||
|
||||
- **Задача (task)** — конфиг поиска: описание цели, ключи, фильтры, план, режим
|
||||
авто-вступления, статус/счётчики. Задач может быть несколько.
|
||||
- **Кандидат (candidate)** — найденный источник (канал/группа, для форумов — оценка по
|
||||
темам). Проходит стадии: `new → evaluated → review → joined | rejected`.
|
||||
- **Метки кандидата** — человекочитаемые пометки: «закрытая группа/канал», «форум»,
|
||||
«не прочитано», «участники не подтверждены», «язык не подтверждён», «есть проходные
|
||||
темы».
|
||||
- **Чёрный список** — источники, отклонённые пользователем; поиск их больше не
|
||||
возвращает (снимается вручную).
|
||||
- **BanGuard** — единый менеджер квот и пауз для всех действий discovery
|
||||
(search/read/join/leave), общий для задач.
|
||||
|
||||
## 4. Задача: конфигурация и правила создания
|
||||
|
||||
Поля задачи:
|
||||
|
||||
| Поле | Назначение | По умолчанию |
|
||||
| --- | --- | --- |
|
||||
| `name` | название задачи | — |
|
||||
| `description` | описание цели (что ищем) | — |
|
||||
| `keywords` | поисковые ключи (генерирует ИИ, редактируются перед стартом) | [] |
|
||||
| `minSubscribers` | минимум участников (0 = не важно) | 0 |
|
||||
| `lang` | язык источников (`ru` / `any`) | `ru` |
|
||||
| `threshold` | доля подходящих сообщений, % | 40 |
|
||||
| `sampleSize` | сколько сообщений смотреть при оценке | 10 |
|
||||
| `planJoins` | план вступлений N | 1..50 |
|
||||
| `autoJoin` | авто-вступление подходящих | false |
|
||||
| `status` | `draft → running → paused → done | failed` | draft |
|
||||
|
||||
Правила создания:
|
||||
|
||||
- **Бюджет планов:** сумма `planJoins` всех задач в статусе не `done/failed` + `planJoins`
|
||||
новой ≤ суточного лимита вступлений (по умолчанию 50). Задача с планом 50 не даёт
|
||||
создать другую; план 25 оставляет максимум 25.
|
||||
- Запуск возможен только после генерации/подтверждения ключей.
|
||||
- При редактировании активной задачи план нельзя увеличить сверх свободного бюджета.
|
||||
|
||||
## 5. Пайплайн поиска (каскад фильтров)
|
||||
|
||||
Выполняется фоновым воркером задачи строго через BanGuard (по одному действию, с паузами).
|
||||
|
||||
Для каждого кандидата фильтры идут **по нарастающей стоимости**; при первом «нет»
|
||||
источник пропускается и берётся следующий:
|
||||
|
||||
1. **Поиск** — `contacts.search` по каждому ключу (с паузами). Кандидаты
|
||||
дедуплицируются по `dialog_id`/username.
|
||||
2. **«Мы не состоим» — глобальный фильтр, применяется сразу и безусловно:** как только
|
||||
источник найден (независимо от запроса/ключей), он отбрасывается, если уже есть
|
||||
в `dialogs` (вступили/мониторим), в чёрном списке или уже обрабатывается/вступил/ждёт
|
||||
рассмотрения в другой задаче (глобальная дедупликация кандидатов). Остальные фильтры
|
||||
(участники/язык/контент) применяются уже после этого. Проверка повторяется
|
||||
непосредственно перед вступлением (между оценкой и join'ом кандидат мог быть добавлен
|
||||
вручную).
|
||||
3. **Число участников** — если `minSubscribers` задано:
|
||||
- значение получено и меньше минимума → пропуск;
|
||||
- значение получить не удалось → **не пропускаем**, ставим метку «участники не
|
||||
подтверждены».
|
||||
4. **Язык** — если `lang=ru`: по выборке сообщений эвристикой кириллицы (без ИИ);
|
||||
не удалось прочитать → метка «язык не подтверждён» (не пропуск).
|
||||
5. **Содержимое** — оценка выборки сообщений (см. §6).
|
||||
|
||||
Пометки «не подтверждено» — не ошибка, а сигнал человеку на экране рассмотрения.
|
||||
|
||||
## 6. Оценка содержимого (по темам, для форумов)
|
||||
|
||||
- **Что считается «подходящим сообщением»:** сообщение проходит те же правила, что в
|
||||
основной системе (этап 1 → ML → ИИ), но **профиль оценки = профиль задачи**
|
||||
(описание + ключи задачи), а не глобальные настройки дашборда. Оценка ничего не
|
||||
создаёт: ни карточек, ни очереди, ни обучения ML.
|
||||
- Если ИИ выключен — оценка локальным разбором/ML.
|
||||
- **Каналы:** читаем до `sampleSize` последних сообщений; доля подходящих ≥ `threshold`
|
||||
→ в «на рассмотрение».
|
||||
- **Открытые группы:** то же; чтение не удалось → «на рассмотрение» с меткой
|
||||
«открытая группа, не прочитана».
|
||||
- **Закрытые группы** (нашлись по ключам, история скрыта): сразу «на рассмотрение» с
|
||||
меткой «закрытая группа/канал» (+ метки неподтверждённых фильтров). Пользователь
|
||||
вступает сам.
|
||||
- **Форумы (группы с темами):** группа раскладывается по темам (`reply_to_top_id`):
|
||||
читаем выборку по активным темам, оценка считается **по темам** («тема: подходит
|
||||
X из N»). Группа подходящая, если есть ≥1 проходная тема. В превью — список тем с
|
||||
пометками проходная/нет. Имена тем, если API не отдаёт без членства, подставляем
|
||||
сниппетом первого сообщения темы.
|
||||
- Порог «40%» применяется к сообщениям темы/канала; если в выборке меньше 3
|
||||
содержательных сообщений — кандидат идёт «на рассмотрение» с меткой «мало сообщений».
|
||||
|
||||
## 7. «На рассмотрение» и действия человека
|
||||
|
||||
Экран по задаче содержит списки: **В обработке / На рассмотрении / Вступили /
|
||||
Отклонены**, плюс история.
|
||||
|
||||
Кандидат на рассмотрении показывает: тип (канал/группа/форум), число участников,
|
||||
метки, долю «подходит X из N» и **почему подошло** (перечень подходящих сообщений/тем
|
||||
с причинами — как блок «попала по фильтру» в карточках), превью сообщений (для
|
||||
форумов — по темам).
|
||||
|
||||
Действия:
|
||||
|
||||
- **«Вступить и мониторить»** — `channels.joinChannel` (по username), добавление в
|
||||
`dialogs` с `monitor=1`, backfill последних ~10 сообщений. Ручной клик — **вне квот**.
|
||||
После вступления источник автоматически попадает под правило «мы состоим» и из
|
||||
поиска исключается.
|
||||
- **«Отклонить»** — источник в чёрный список (исключается из поиска во всех задачах).
|
||||
Если для оценки пришлось вступать — выходим (`channels.leaveChannel`) в рамках квот.
|
||||
Чёрный список редактируется вручную (можно снять).
|
||||
- **Закрытые группы:** вместо авто-вступления — кнопка-ссылка `t.me/<username>`; система
|
||||
замечает вступление при синхронизации диалогов и предлагает добавить источник в
|
||||
мониторинг (метка «вступили, добавить в мониторинг?»).
|
||||
|
||||
## 8. Авто-вступление, квоты и анти-бан (BanGuard)
|
||||
|
||||
- Суточный лимит вступлений — **50** (настройка), общий для всех задач, считаются только
|
||||
автоматические вступления. Ручные — без ограничений.
|
||||
- Авто-вступление включается на задачу (`autoJoin`). Подходящие кандидаты вступают сами.
|
||||
- Интервалы между автоматическими вступлениями: **случайно 50–70 секунд**; по одному
|
||||
действию, без параллелей. Поиск и чтение — мягкие паузы (единицы секунд + джиттер,
|
||||
переиспользуем значения анти-бана из telegram.py).
|
||||
- Задача «выполнена» при достижении плана вступлений. Если за сутки упёрлись в общий
|
||||
бюджет — авто-режим продолжает на следующий день (новый суточный бюджет).
|
||||
- `FloodWaitError` → пауза по секундам из ответа + запас; авто-вступления останавливаются
|
||||
до следующего дня при флуде. Общий «стоп-кран» — пауза всего discovery.
|
||||
- Все квоты/интервалы — настройки в UI.
|
||||
|
||||
## 9. Хранилище
|
||||
|
||||
| Таблица | Назначение / ключевые поля |
|
||||
| --- | --- |
|
||||
| `disc_tasks` | задачи: name, description, keywords(JSON), min_subscribers, lang, threshold, sample_size, plan_joins, auto_join, status, counters (found/evaluated/joined/rejected), created/updated |
|
||||
| `disc_candidates` | dialog_id/username/name/kind(channel|group|forum)/hue, participants, lang_ru, join_failures, marks(JSON), topics(JSON: {topicId,title,fitCount,total,fitRatio,passed}), fit_ratio, status(new/review/joined/rejected), task_id, times |
|
||||
| `disc_blacklist` | dialog_id, name, reason, created_at |
|
||||
| `disc_log` | история задачи: task_id, event(search/evaluate/join/leave/flood/error/review/blacklist), text, created_at |
|
||||
|
||||
Дубли кандидатов не создаются; источник, попавший в другую задачу или `dialogs`,
|
||||
из поиска исключается (правило «мы состоим» — глобальное).
|
||||
|
||||
Реализация: у кандидата нет транзитного статуса `evaluated` (счётчик оценённых — на
|
||||
задаче); `join_failures` — неудачные авто-вступления подряд, после 3 кандидат удаляется.
|
||||
|
||||
## 10. API
|
||||
|
||||
- `GET/POST/PATCH/DELETE /api/discovery/tasks` (создание с валидацией бюджета планов),
|
||||
`POST /api/discovery/tasks/{id}/start|pause`
|
||||
- `POST /api/discovery/tasks/{id}/generate-keywords` — ИИ генерирует ключи по описанию
|
||||
- `GET /api/discovery/tasks/{id}/candidates?status=review|joined|rejected`
|
||||
- `POST /api/discovery/candidates/{id}/join` (вступить и мониторить), `.../reject`
|
||||
- `GET /api/discovery/blacklist`, `DELETE /api/discovery/blacklist/{dialog_id}`
|
||||
- `GET /api/discovery/tasks/{id}/log`
|
||||
|
||||
## 11. UI
|
||||
|
||||
Подвкладка **«Поиск»** на экране «Каналы»:
|
||||
- список задач (статус, прогресс, план/вступили, авто-режим) + «Новая задача»;
|
||||
- мастер задачи: описание → «Сгенерировать ключи ИИ» → редактирование ключей →
|
||||
фильтры/план/авто-режим → запуск;
|
||||
- по задаче: статус-лента (поиск → оценка → вступление), вкладки «В обработке /
|
||||
На рассмотрении / Вступили / Отклонены», история;
|
||||
- кандидат на рассмотрении раскрывается с превью и действиями; форум — по темам;
|
||||
- настройки квот (лимит/интервалы) — в том же экране или «Настройки → Telegram».
|
||||
|
||||
## 12. Интеграция с существующим кодом
|
||||
|
||||
- Фоновый воркер discovery — отдельный цикл в `main.py` (как `_pipeline_loop`),
|
||||
сервис `app/services/discovery.py`, Telegram-действия — методы `TelegramManager`
|
||||
(поиск/join/leave/чтение) с общим pacing.
|
||||
- Переиспользуем: `rules/stage1/ML/AI` для оценки сообщений (новый лёгкий вызов с
|
||||
профилем задачи, без записи карточек), список `dialogs` для фильтра «мы состоим»,
|
||||
синхронизацию диалогов для авто-добавления закрытых групп.
|
||||
- К основному пайплайну карточек, ML и ТЗ-логике не прикасаемся.
|
||||
|
||||
## 13. Вне рамок (сейчас)
|
||||
|
||||
- Агрегаторы-каталоги как источник кандидатов.
|
||||
- «Похожие каналы» (`getChannelRecommendations`) от наших подписок — отдельная опция позже.
|
||||
- Авто-вступление в закрытые группы по инвайт-ссылкам (глобальным поиском они не находятся).
|
||||
|
||||
## 14. Значения по умолчанию (настраиваются в UI)
|
||||
|
||||
суточный лимит вступлений = 50; интервал авто-вступлений = 50–70 с; выборка = 10
|
||||
сообщений; порог = 40%; мин. содержательных сообщений для оценки = 3; язык = ru.
|
||||
|
||||
@@ -1,86 +1,86 @@
|
||||
# Открытые вопросы: вложения источников (media → Storage) и просмотр исходника
|
||||
|
||||
Дата: 2026-09-11. Статус: решения владельца получены (см. §0).
|
||||
|
||||
## 0. Решения владельца (2026-09-11)
|
||||
|
||||
- **А) Медиа-посты пропускаем.** Сообщения без текста (только медиа/вложение) в систему не попадают.
|
||||
Извлечение вложений Telegram и выгрузка их в Storage не делаются. Generic-контракт по-прежнему умеет
|
||||
нести `DataRef` — этим смогут пользоваться другие источники (файл/диск/таблица) и ручные вложения карточки.
|
||||
- **Б) Проверка без живого Telegram** — реализуем с юнит-тестами на фейковой сессии/фейковом Storage,
|
||||
без реального API.
|
||||
- **В)** Объяснение термина — в §1.5. **Решение: делаем.** Реализован remote-просмотр: `TelegramService.ReadSource`,
|
||||
`ITelegramGateway.ReadSourceAsync`, `TelegramSourceContentProvider` (Kind=telegram) в ядре,
|
||||
`GET /api/cards/{id}/source` и кнопка «Обновить из источника» в подробной карточке.
|
||||
|
||||
## 1.5. Что такое «remote-просмотр исходника»
|
||||
|
||||
Карточка хранит **ссылку на источник** (`SourceRef`) и **содержимое** (`SourceContent`). Содержимое попадает
|
||||
в карточку в момент приёма. «Просмотр исходника» — это возможность по кнопке догрузить/показать **оригинальное
|
||||
сообщение у источника** (то, что было в канале/письме/строке), если контент в карточке устарел или урезан.
|
||||
|
||||
Сейчас содержимое уже отдаётся в `CardDto.content` и через `GET /api/cards/{id}/source`. Для локальных
|
||||
источников этого достаточно. Для **внешних** источников (например Telegram) данные лежат не в ядре, а в
|
||||
сервисе-владельце; чтобы их догрузить, ядру нужен провайдер `ISourceContentProvider` для `kind`, который
|
||||
ходит по gRPC к сервису-владельцу (условный RPC `ReadSource(dialogId, msgId)`) и возвращает исходный текст/медиа.
|
||||
|
||||
Это и есть «remote-просмотр» — расширение extension-point, которое не требуется до появления реальной
|
||||
необходимости (напр. если карточки хранят урезанный текст или нужно открыть живой первоисточник).
|
||||
|
||||
## 1. Что уже готово (не требует решений)
|
||||
|
||||
- Единый контракт источника несёт вложения: `SourceContent.Data: IReadOnlyList<DataRef>` —
|
||||
ссылки на объекты Storage-сервиса (`DataRef.Id/Ref/Kind/MimeType/...`).
|
||||
- Контракт входящего потока (`src/contracts/sources.proto`, `PushSource`) передаёт
|
||||
`DataRefProto`/`ContactRefProto` — источник может прислать вложения сразу со ссылками.
|
||||
- Storage-сервис (`src/storage-service/Deal.Storage`, `storage.proto`) умеет `Upload/Download/Stat/Delete`,
|
||||
сам определяет `kind`/`mimeType`/размеры (контент-снифинг), бэкенд — MinIO.
|
||||
- Ядро хранит `SourceContent` карточки (в т.ч. `Data`) и отдаёт его в `CardDto.content` и через
|
||||
`GET /api/cards/{id}/source` (extension-point `ISourceContentProvider` + `SourceContentResolver`).
|
||||
- Фронт рендерит вложения: `SourceContentView.vue` (image/video/audio/document/archive по `kind`),
|
||||
ссылки и контакты — списками.
|
||||
|
||||
## 2. Проблемная часть (требует живого Telegram)
|
||||
|
||||
Извлечение и выгрузка медиа из Telegram не проверяемы офлайн:
|
||||
|
||||
1. **Медиа-сообщения сейчас отбрасываются.** `TlMessageMapper.ToMessage`
|
||||
(`src/telegram-service/Deal.Telegram/Telegram/TlMessageMapper.cs`) принимает только `Message`
|
||||
с непустым `message` (текстом). Посты с одним вложением и подписью (`media` + `caption`) не попадают
|
||||
в поток вообще. Нужно: определять `Message.media`, читать `caption`, тип/размеры/длительность.
|
||||
2. **Скачивание и выгрузка.** Требуется `client.DownloadMedia(...)` (WTelegram) → поток →
|
||||
`StorageService.Upload(meta + data)` → `DataRefProto`. В telegram-сервисе нет gRPC-клиента Storage
|
||||
и соответствующей конфигурации в compose (endpoint/токен). Проверить можно только с реальным
|
||||
Telegram-аккаунтом и живым MinIO.
|
||||
3. **Подпись без текста.** Даже если вложение извлечено, в посте может не быть текста: нужен ли такой
|
||||
пост «карточкой» (сейчас `PipelineIngestService` пропускает записи без `Content.Text`)? Предлагается
|
||||
принимать запись, если есть текст **или** вложения/ссылки/контакты, а классификацию медиа-онли
|
||||
строить по подписи (`caption`) и метаданным. Требуется подтверждение продуктовой логики.
|
||||
4. **Просмотр исходника из другого контура.** «Открыть исходник» для remote-источников (Telegram — это
|
||||
лишь один из них) требует провайдера `ISourceContentProvider`, который ходит по gRPC к сервису-владельцу
|
||||
источника (новый RPC, например `ReadSource(dialogId, msgId)`), возвращая текст/медиа. Это тоже
|
||||
живой Telegram.
|
||||
|
||||
## 3. Предлагаемый план (после подтверждения)
|
||||
|
||||
1. `TelegramMessage` расширить моделью `TelegramAttachment` (caption, fileName, mimeType, size, width,
|
||||
height, durationSec, `Task<Stream> Open(cancellationToken)`), заполнять в `TlMessageMapper` из
|
||||
`Message.media`/`Document`/`Photo`.
|
||||
2. В telegram-сервисе добавить `StorageClient` (gRPC, `Deal.Grpc.Storage`) + `SourceAttachmentUploader`:
|
||||
загрузка каждого вложения → `DataRefProto`.
|
||||
3. `DialogProtoMapper.ToSourceRequest(message, dataRefs)` — прокинуть `content.data` и `caption`.
|
||||
4. `PipelineIngestService`: принимать запись при непустом тексте **или** непустых `Data`/`Links`/`Contacts`
|
||||
(нужно продуктовое решение по п.2.3).
|
||||
5. `ISourceContentProvider` для `kind="telegram"` — gRPC-провайдер к telegram-сервису (RPC `ReadSource`).
|
||||
6. Настройки: endpoint/токен Storage в `deploy/compose.dev.yml`/`compose.prod.yml` для telegram-сервиса.
|
||||
|
||||
## 4. Что нужно от владельца
|
||||
|
||||
- А) Делать ли медиа-сообщения без текста карточками (по подписи/метаданным), или пропускать?
|
||||
- Б) Для проверки вложений нужны живые Telegram api_id/api_hash и работающий MinIO — будет ли прогон
|
||||
на вашей стороне, или реализуем «слепо» с юнит-тестами на фейковой сессии и фейковом Storage?
|
||||
- В) Нужен ли remote-просмотр исходника (`ReadSource`) в этом объёме, или достаточно того, что
|
||||
содержимое хранится в карточке?
|
||||
|
||||
Пока эти пункты не закрыты, они вынесены в `backlog.md` (`TD-STORE-ATTACH`, `TD-SOURCE-PROVIDER`),
|
||||
а generic-часть (контракт, Storage-сервис, хранение, API, рендер) реализована.
|
||||
# Открытые вопросы: вложения источников (media → Storage) и просмотр исходника
|
||||
|
||||
Дата: 2026-09-11. Статус: решения владельца получены (см. §0).
|
||||
|
||||
## 0. Решения владельца (2026-09-11)
|
||||
|
||||
- **А) Медиа-посты пропускаем.** Сообщения без текста (только медиа/вложение) в систему не попадают.
|
||||
Извлечение вложений Telegram и выгрузка их в Storage не делаются. Generic-контракт по-прежнему умеет
|
||||
нести `DataRef` — этим смогут пользоваться другие источники (файл/диск/таблица) и ручные вложения карточки.
|
||||
- **Б) Проверка без живого Telegram** — реализуем с юнит-тестами на фейковой сессии/фейковом Storage,
|
||||
без реального API.
|
||||
- **В)** Объяснение термина — в §1.5. **Решение: делаем.** Реализован remote-просмотр: `TelegramService.ReadSource`,
|
||||
`ITelegramGateway.ReadSourceAsync`, `TelegramSourceContentProvider` (Kind=telegram) в ядре,
|
||||
`GET /api/cards/{id}/source` и кнопка «Обновить из источника» в подробной карточке.
|
||||
|
||||
## 1.5. Что такое «remote-просмотр исходника»
|
||||
|
||||
Карточка хранит **ссылку на источник** (`SourceRef`) и **содержимое** (`SourceContent`). Содержимое попадает
|
||||
в карточку в момент приёма. «Просмотр исходника» — это возможность по кнопке догрузить/показать **оригинальное
|
||||
сообщение у источника** (то, что было в канале/письме/строке), если контент в карточке устарел или урезан.
|
||||
|
||||
Сейчас содержимое уже отдаётся в `CardDto.content` и через `GET /api/cards/{id}/source`. Для локальных
|
||||
источников этого достаточно. Для **внешних** источников (например Telegram) данные лежат не в ядре, а в
|
||||
сервисе-владельце; чтобы их догрузить, ядру нужен провайдер `ISourceContentProvider` для `kind`, который
|
||||
ходит по gRPC к сервису-владельцу (условный RPC `ReadSource(dialogId, msgId)`) и возвращает исходный текст/медиа.
|
||||
|
||||
Это и есть «remote-просмотр» — расширение extension-point, которое не требуется до появления реальной
|
||||
необходимости (напр. если карточки хранят урезанный текст или нужно открыть живой первоисточник).
|
||||
|
||||
## 1. Что уже готово (не требует решений)
|
||||
|
||||
- Единый контракт источника несёт вложения: `SourceContent.Data: IReadOnlyList<DataRef>` —
|
||||
ссылки на объекты Storage-сервиса (`DataRef.Id/Ref/Kind/MimeType/...`).
|
||||
- Контракт входящего потока (`src/contracts/sources.proto`, `PushSource`) передаёт
|
||||
`DataRefProto`/`ContactRefProto` — источник может прислать вложения сразу со ссылками.
|
||||
- Storage-сервис (`src/storage-service/Deal.Storage`, `storage.proto`) умеет `Upload/Download/Stat/Delete`,
|
||||
сам определяет `kind`/`mimeType`/размеры (контент-снифинг), бэкенд — MinIO.
|
||||
- Ядро хранит `SourceContent` карточки (в т.ч. `Data`) и отдаёт его в `CardDto.content` и через
|
||||
`GET /api/cards/{id}/source` (extension-point `ISourceContentProvider` + `SourceContentResolver`).
|
||||
- Фронт рендерит вложения: `SourceContentView.vue` (image/video/audio/document/archive по `kind`),
|
||||
ссылки и контакты — списками.
|
||||
|
||||
## 2. Проблемная часть (требует живого Telegram)
|
||||
|
||||
Извлечение и выгрузка медиа из Telegram не проверяемы офлайн:
|
||||
|
||||
1. **Медиа-сообщения сейчас отбрасываются.** `TlMessageMapper.ToMessage`
|
||||
(`src/telegram-service/Deal.Telegram/Telegram/TlMessageMapper.cs`) принимает только `Message`
|
||||
с непустым `message` (текстом). Посты с одним вложением и подписью (`media` + `caption`) не попадают
|
||||
в поток вообще. Нужно: определять `Message.media`, читать `caption`, тип/размеры/длительность.
|
||||
2. **Скачивание и выгрузка.** Требуется `client.DownloadMedia(...)` (WTelegram) → поток →
|
||||
`StorageService.Upload(meta + data)` → `DataRefProto`. В telegram-сервисе нет gRPC-клиента Storage
|
||||
и соответствующей конфигурации в compose (endpoint/токен). Проверить можно только с реальным
|
||||
Telegram-аккаунтом и живым MinIO.
|
||||
3. **Подпись без текста.** Даже если вложение извлечено, в посте может не быть текста: нужен ли такой
|
||||
пост «карточкой» (сейчас `PipelineIngestService` пропускает записи без `Content.Text`)? Предлагается
|
||||
принимать запись, если есть текст **или** вложения/ссылки/контакты, а классификацию медиа-онли
|
||||
строить по подписи (`caption`) и метаданным. Требуется подтверждение продуктовой логики.
|
||||
4. **Просмотр исходника из другого контура.** «Открыть исходник» для remote-источников (Telegram — это
|
||||
лишь один из них) требует провайдера `ISourceContentProvider`, который ходит по gRPC к сервису-владельцу
|
||||
источника (новый RPC, например `ReadSource(dialogId, msgId)`), возвращая текст/медиа. Это тоже
|
||||
живой Telegram.
|
||||
|
||||
## 3. Предлагаемый план (после подтверждения)
|
||||
|
||||
1. `TelegramMessage` расширить моделью `TelegramAttachment` (caption, fileName, mimeType, size, width,
|
||||
height, durationSec, `Task<Stream> Open(cancellationToken)`), заполнять в `TlMessageMapper` из
|
||||
`Message.media`/`Document`/`Photo`.
|
||||
2. В telegram-сервисе добавить `StorageClient` (gRPC, `Deal.Grpc.Storage`) + `SourceAttachmentUploader`:
|
||||
загрузка каждого вложения → `DataRefProto`.
|
||||
3. `DialogProtoMapper.ToSourceRequest(message, dataRefs)` — прокинуть `content.data` и `caption`.
|
||||
4. `PipelineIngestService`: принимать запись при непустом тексте **или** непустых `Data`/`Links`/`Contacts`
|
||||
(нужно продуктовое решение по п.2.3).
|
||||
5. `ISourceContentProvider` для `kind="telegram"` — gRPC-провайдер к telegram-сервису (RPC `ReadSource`).
|
||||
6. Настройки: endpoint/токен Storage в `deploy/compose.dev.yml`/`compose.prod.yml` для telegram-сервиса.
|
||||
|
||||
## 4. Что нужно от владельца
|
||||
|
||||
- А) Делать ли медиа-сообщения без текста карточками (по подписи/метаданным), или пропускать?
|
||||
- Б) Для проверки вложений нужны живые Telegram api_id/api_hash и работающий MinIO — будет ли прогон
|
||||
на вашей стороне, или реализуем «слепо» с юнит-тестами на фейковой сессии и фейковом Storage?
|
||||
- В) Нужен ли remote-просмотр исходника (`ReadSource`) в этом объёме, или достаточно того, что
|
||||
содержимое хранится в карточке?
|
||||
|
||||
Пока эти пункты не закрыты, они вынесены в `backlog.md` (`TD-STORE-ATTACH`, `TD-SOURCE-PROVIDER`),
|
||||
а generic-часть (контракт, Storage-сервис, хранение, API, рендер) реализована.
|
||||
|
||||
@@ -1,193 +1,193 @@
|
||||
# Дизайн: единый контракт источника + общий Storage-сервис данных
|
||||
|
||||
Дата: 2026-09-11. Статус: реализовано в ядре (домен, Storage-сервис, персистентность, конвейер, wire, фронт); адаптер/провайдер telegram-сервиса и Storage-выгрузка — следующие шаги. Контракт не плодит типы вложений; файлы — в общем Storage.
|
||||
|
||||
## 1. Принцип
|
||||
|
||||
1. **Единый строго типизированный контракт.** Любой источник (Telegram, WhatsApp, Avito, сайт, файл,
|
||||
Excel) через адаптер приводит данные к одному типу `SourceItem`. Ядро, AI и ML работают только с ним.
|
||||
2. **Данные файлов — в общем Storage-сервисе.** Каждый сервис-источник сам выгружает свои данные
|
||||
(картинки, видео, аудио, документы, любые файлы) в общий Storage с **токеном валидации**. Storage сам
|
||||
определяет тип и метаданные. В контракте хранится **ссылка** на файл, а не сам файл.
|
||||
3. **Никаких подтипов вложений в контракте.** Не плодим `ImagePart/VideoPart/...`; есть универсальный
|
||||
`DataRef` с полем `Kind`, которое заполняет Storage.
|
||||
4. Ссылки, контакты и прочее, что **не является файлом**, идут отдельными полями контента.
|
||||
5. В ядре нет Telegram-полей и слова Telegram (только в telegram-сервисе); в комментариях нет упоминаний
|
||||
задач/этапов/ТЗ.
|
||||
|
||||
## 2. Единый контракт (Deal.Modules.Cards)
|
||||
|
||||
```csharp
|
||||
public sealed record SourceItem
|
||||
{
|
||||
public required SourceRef Source { get; init; }
|
||||
public required SourceContent Content { get; init; }
|
||||
}
|
||||
|
||||
public sealed record SourceRef
|
||||
{
|
||||
public required string Kind { get; init; } // "telegram", "whatsapp", "avito", "file", "excel", ...
|
||||
public string? ExternalId { get; init; } // id в источнике (сообщение/строка/файл)
|
||||
public string? DisplayName { get; init; } // подпись в UI
|
||||
public string? OriginRef { get; init; } // url / deep-link / путь
|
||||
public string? Author { get; init; }
|
||||
public DateTimeOffset ReceivedAt { get; init; }
|
||||
public IReadOnlyDictionary<string, string>? Extra { get; init; }
|
||||
}
|
||||
|
||||
public sealed record SourceContent
|
||||
{
|
||||
public string? Text { get; init; } // основной текст
|
||||
public string? Html { get; init; } // разметка (если есть)
|
||||
public string? Author { get; init; } // отправитель
|
||||
public string? Subject { get; init; } // тема/заголовок
|
||||
public IReadOnlyList<DataRef> Data { get; init; } = []; // ссылки на файлы в Storage
|
||||
public IReadOnlyList<string>? Links { get; init; } // ссылки (не файлы)
|
||||
public IReadOnlyList<ContactRef>? Contacts { get; init; } // контакты
|
||||
public IReadOnlyDictionary<string, string>? Extra { get; init; } // прочее (не файл/не ссылка/не контакт)
|
||||
}
|
||||
```
|
||||
|
||||
`DataRef` — ссылка на объект в Storage; тип и метаданные определил Storage (nullable, чтобы не плодить типы):
|
||||
|
||||
```csharp
|
||||
public sealed record DataRef
|
||||
{
|
||||
public required string Id { get; init; } // идентификатор объекта в Storage
|
||||
public required string Ref { get; init; } // ссылка (url/путь) для скачивания/отображения
|
||||
public string? Kind { get; init; } // определил Storage: image/video/audio/document/archive/other
|
||||
public string? MimeType { get; init; }
|
||||
public string? FileName { get; init; }
|
||||
public long? Size { get; init; }
|
||||
public int? Width { get; init; }
|
||||
public int? Height { get; init; }
|
||||
public double? DurationSec { get; init; }
|
||||
public string? PreviewRef { get; init; } // превью/thumbnail
|
||||
public string? Caption { get; init; }
|
||||
public int? Order { get; init; }
|
||||
public IReadOnlyDictionary<string, string>? Meta { get; init; } // прочие метаданные от Storage
|
||||
}
|
||||
```
|
||||
|
||||
`ContactRef`: `Name?`, `Phone?`, `Email?`, `Url?`, `Kind?` (контакт может быть квалифицирован).
|
||||
|
||||
## 3. Storage-сервис (общий)
|
||||
|
||||
Отдельный сервис (как ai/ml/telegram), владелец — данные. Источники и ядро только ссылаются на объекты.
|
||||
|
||||
- **Загрузка:** `Upload(stream, token, fileName?) → DataRef`. Каждый сервис-источник выгружает свои данные
|
||||
**сам**, передавая **токен валидации** (сервисный токен/mTLS — уже есть в gRPC-обвязке).
|
||||
- **Определение типа:** Storage сам решает `Kind`/`MimeType`/размеры/длительность (контент-снифинг);
|
||||
контракт типы не задаёт.
|
||||
- **Чтение:** `Get(id) → (stream, DataRef)` либо выдача ссылки/временного URL.
|
||||
- **Бэкенд:** объектное хранилище (MinIO/S3). Путь/бакет — по тенанту.
|
||||
- **Владение:** единый общий сервис; каждый источник пишет в него со своим токеном, ядро/AI/ML читают по ссылке.
|
||||
|
||||
## 4. Адаптеры источников
|
||||
|
||||
```csharp
|
||||
public interface ISourceAdapter { string Kind { get; } SourceItem Normalize(object native); }
|
||||
```
|
||||
|
||||
Владельцы: `telegram` → telegram-сервис; `local` → ручное создание (Cards); `whatsapp`/`avito`/`web`/`file`/
|
||||
`excel` → соответствующий сервис. Файлы адаптер сам выгружает в Storage и кладёт в контракт `DataRef`.
|
||||
|
||||
## 5. Загрузка исходника карточки
|
||||
|
||||
Единый способ: по `SourceRef.Kind` — провайдер, возвращающий `SourceContent` (для файла — через Storage по
|
||||
`DataRef.Ref`, для сообщения — у источника). `ISourceContentProvider { Kind; LoadAsync(SourceRef) }` + реестр.
|
||||
API ядра: `GET /api/cards/{id}/source` → generic контент.
|
||||
|
||||
## 6. Персистентность
|
||||
|
||||
В карточках вместо плоских Telegram-колонок:
|
||||
|
||||
- `SourceKind` (text); `SourceJson` (jsonb, `SourceRef`);
|
||||
- `ContentJson` (jsonb, `SourceContent` — текст + `DataRef`-ссылки + прочее);
|
||||
- `SourceText` (text, FTS);
|
||||
- `SourceRefUrl` (text?, `OriginRef`).
|
||||
|
||||
Конвертер контента общий (без per-source сериализаторов). Миграции: старые удаляем → новый init с нуля.
|
||||
|
||||
## 7. Wire и фронт
|
||||
|
||||
- `CardDto.Source` = `{ kind, displayName?, originRef?, receivedAt }`.
|
||||
- `GET /api/cards/{id}/source` → `{ text?, html?, author?, subject?, data[], links[], contacts[], extra? }`.
|
||||
- Фронт: generic блок источника + универсальный просмотрщик (по `DataRef.Kind` — картинка/видео/аудио/файл;
|
||||
ссылки/контакты — списками).
|
||||
|
||||
## 8. Этапы
|
||||
|
||||
1. Домен: `SourceItem/SourceRef/SourceContent/DataRef/ContactRef`; удалить Telegram-маркеры из Cards.
|
||||
2. Storage-сервис: контракт gRPC, определение типа, токен валидации, бэкенд MinIO; регистрация.
|
||||
3. Персистентность: `SourceKind/SourceJson/ContentJson/SourceText/SourceRefUrl`, общий конвертер, новый init,
|
||||
маппинг KanbanStore.
|
||||
4. Pipeline: приём `SourceItem`, загрузка вложений в Storage адаптером, без Telegram-полей.
|
||||
5. Wire/API: generic `Source` в `CardDto`, `GET /api/cards/{id}/source`, провайдеры.
|
||||
6. Frontend: generic источник + универсальный просмотрщик.
|
||||
7. Telegram: адаптер + провайдер исходника (только в telegram-сервисе) + выгрузка в Storage.
|
||||
8. Комментарии: убрать упоминания Telegram из ядра и задачи/этапы — везде.
|
||||
|
||||
## 9. Реализация: зафиксированные сигнатуры
|
||||
|
||||
### Ядро: домен
|
||||
|
||||
- `SourceRefs` (Deal.Modules.Cards/Application/Sources): `Empty`, `DefaultHue = "#666"`,
|
||||
`HueKey = "hue"`, расширения `DedupeKey()` (вид|оригинал|внешний id), `ResolveHue()`.
|
||||
- `CardSnapshot`: вместо `ChannelName/ChannelHandle/ChannelHue/SourceMsg/SourceDialogId/SourceMsgId` —
|
||||
`SourceRef Source` + `SourceContent Content`; `ReceivedAt` остаётся.
|
||||
- `CardDto`: вместо `Channel`/`SourceMsg`/`SourceDialogId`/`SourceMsgId`/прежнего `Source` —
|
||||
`SourceRef Source` + `SourceContent Content`; `ReceivedAtMs` остаётся. `CardChannelDto`/`CardSourceDto` удалены.
|
||||
- `ICardStore.GetCardBySourceAsync(SourceRef source, CancellationToken ct)`.
|
||||
|
||||
### Ядро: конвейер
|
||||
|
||||
- `QueuedMessage { required SourceItem Item; bool Force; }`.
|
||||
- `QueueItemDto { string Id; SourceRef Source; SourceContent Content; string Text; string Status;
|
||||
long MsgAtMs; long QueuedAtMs; bool Force; }` (JsonIgnore на Force).
|
||||
- `RejectRecord { SourceRef Source; SourceContent Content; string Text; long MsgAtMs;
|
||||
string DecidedBy; string Stage; string Reason; string Kw; string? DeterministicId; }`
|
||||
(`DeterministicId = r_{Kind}_{OriginRef}_{ExternalId}`).
|
||||
- `RejectedItemDto`: `Source`/`Content`, `DecidedBy`/`DecidedByLabel` (решение), остальное как было.
|
||||
- `IPipelineStore.ExistsDuplicateAsync(SourceRef source, CancellationToken ct)`.
|
||||
- `PipelineChannelDto` удалён.
|
||||
|
||||
### Схема БД (схема тенанта)
|
||||
|
||||
- Cards: удалить `ChannelName/ChannelHandle/ChannelHue/SourceMsg/SourceDialogId/SourceMsgId`;
|
||||
добавить `SourceKind`, `SourceExternalId`, `SourceOriginRef` (text, для запросов), `SourceJson` (text),
|
||||
`ContentJson` (text), `SourceText` (text). FTS: Title+Summary+SourceText+Contact.
|
||||
- QueueItems: удалить `DialogId/ChannelName/ChannelHandle/ChannelHue/MsgId`; добавить
|
||||
`SourceKey` (text, уникальный ключ дедупа), `SourceJson`, `ContentJson`. `Text/MsgAt/Status/Force/CreatedAt/UpdatedAt` остаются.
|
||||
- RejectedItems: удалить `DialogId/MsgId/ChannelName/ChannelHandle/ChannelHue`; добавить
|
||||
`SourceKey`, `SourceJson`, `ContentJson`. FTS — по `Text`.
|
||||
- Миграции tenant: старые удалить, сгенерировать новый init с нуля (данных нет).
|
||||
|
||||
### Маппинг источника
|
||||
|
||||
- Telegram-адаптер (в ядре — тонкий край приёма): `Kind="telegram"`, `ExternalId=MsgId`,
|
||||
`OriginRef=DialogId`, `DisplayName=ChannelName`, `Extra["hue"]=ChannelHue` (иначе дефолт),
|
||||
`ReceivedAt=msgAt`, `Content.Text=Text`, `Content.Author=ChannelName`.
|
||||
- Дашборды/карточки/конвейер работают только с `SourceRef`/`SourceContent`; Telegram-поля не проходят дальше адаптера.
|
||||
|
||||
### Входящий поток (generic, 2026-09-11)
|
||||
|
||||
- `src/contracts/sources.proto` → сервис `SourceIngressService.PushSource` с generic-типами
|
||||
`SourceRefProto`/`SourceContentProto`/`DataRefProto`/`ContactRefProto`; вложения — ссылки на Storage.
|
||||
- Ядро: `Deal.Api/Sources/SourceIngressGrpcService` (приём) + `SourceProtoMapper` (proto → домен) +
|
||||
`ISourceIngestObserver` (вторичная обработка принятой записи, сбой наблюдателя не влияет на приём) +
|
||||
`IngressTenantResolver` (тенант по metadata).
|
||||
- Из `telegram.proto` удалён `IngressService.PushMessage` (остались `SyncDialogs`/`ReportStatus`);
|
||||
telegram-сервис шлёт записи через `PushSource` (`kind="telegram"`). Превью каталога/TgMessages сохраняет
|
||||
`TelegramSourceIngestObserver` (ядро, telegram-модуль — единственное место с telegram-спецификой приёма).
|
||||
- Любой другой источник (whatsapp/avito/файл/excel) шлёт тот же `PushSource` со своим `source.kind`.
|
||||
|
||||
### Remote-просмотр исходника (2026-09-11)
|
||||
|
||||
- `TelegramService.ReadSource(ReadSourceRequest{dialog_id, msg_id})` → `ReadSourceReply{found, text?, time?}`
|
||||
(`src/contracts/telegram.proto`); telegram-сервис достаёт конкретное сообщение
|
||||
(`ISessionClient.GetMessageAsync` → TL `Messages_GetMessages`). Медиа без текста → `found=false`.
|
||||
- Ядро: `ITelegramGateway.ReadSourceAsync` + `TelegramSourceContentProvider` (`ISourceContentProvider`,
|
||||
`Kind="telegram"`, `Deal.Infrastructure/Integrations/Sources`) — резолвится `SourceContentResolver`.
|
||||
- `GET /api/cards/{id}/source` отдаёт результат провайдера либо сохранённое содержимое карточки.
|
||||
Фронт: кнопка «Обновить из источника» в подробной карточке (`CardDrawer.vue` → `loadCardSource`).
|
||||
# Дизайн: единый контракт источника + общий Storage-сервис данных
|
||||
|
||||
Дата: 2026-09-11. Статус: реализовано в ядре (домен, Storage-сервис, персистентность, конвейер, wire, фронт); адаптер/провайдер telegram-сервиса и Storage-выгрузка — следующие шаги. Контракт не плодит типы вложений; файлы — в общем Storage.
|
||||
|
||||
## 1. Принцип
|
||||
|
||||
1. **Единый строго типизированный контракт.** Любой источник (Telegram, WhatsApp, Avito, сайт, файл,
|
||||
Excel) через адаптер приводит данные к одному типу `SourceItem`. Ядро, AI и ML работают только с ним.
|
||||
2. **Данные файлов — в общем Storage-сервисе.** Каждый сервис-источник сам выгружает свои данные
|
||||
(картинки, видео, аудио, документы, любые файлы) в общий Storage с **токеном валидации**. Storage сам
|
||||
определяет тип и метаданные. В контракте хранится **ссылка** на файл, а не сам файл.
|
||||
3. **Никаких подтипов вложений в контракте.** Не плодим `ImagePart/VideoPart/...`; есть универсальный
|
||||
`DataRef` с полем `Kind`, которое заполняет Storage.
|
||||
4. Ссылки, контакты и прочее, что **не является файлом**, идут отдельными полями контента.
|
||||
5. В ядре нет Telegram-полей и слова Telegram (только в telegram-сервисе); в комментариях нет упоминаний
|
||||
задач/этапов/ТЗ.
|
||||
|
||||
## 2. Единый контракт (Deal.Modules.Cards)
|
||||
|
||||
```csharp
|
||||
public sealed record SourceItem
|
||||
{
|
||||
public required SourceRef Source { get; init; }
|
||||
public required SourceContent Content { get; init; }
|
||||
}
|
||||
|
||||
public sealed record SourceRef
|
||||
{
|
||||
public required string Kind { get; init; } // "telegram", "whatsapp", "avito", "file", "excel", ...
|
||||
public string? ExternalId { get; init; } // id в источнике (сообщение/строка/файл)
|
||||
public string? DisplayName { get; init; } // подпись в UI
|
||||
public string? OriginRef { get; init; } // url / deep-link / путь
|
||||
public string? Author { get; init; }
|
||||
public DateTimeOffset ReceivedAt { get; init; }
|
||||
public IReadOnlyDictionary<string, string>? Extra { get; init; }
|
||||
}
|
||||
|
||||
public sealed record SourceContent
|
||||
{
|
||||
public string? Text { get; init; } // основной текст
|
||||
public string? Html { get; init; } // разметка (если есть)
|
||||
public string? Author { get; init; } // отправитель
|
||||
public string? Subject { get; init; } // тема/заголовок
|
||||
public IReadOnlyList<DataRef> Data { get; init; } = []; // ссылки на файлы в Storage
|
||||
public IReadOnlyList<string>? Links { get; init; } // ссылки (не файлы)
|
||||
public IReadOnlyList<ContactRef>? Contacts { get; init; } // контакты
|
||||
public IReadOnlyDictionary<string, string>? Extra { get; init; } // прочее (не файл/не ссылка/не контакт)
|
||||
}
|
||||
```
|
||||
|
||||
`DataRef` — ссылка на объект в Storage; тип и метаданные определил Storage (nullable, чтобы не плодить типы):
|
||||
|
||||
```csharp
|
||||
public sealed record DataRef
|
||||
{
|
||||
public required string Id { get; init; } // идентификатор объекта в Storage
|
||||
public required string Ref { get; init; } // ссылка (url/путь) для скачивания/отображения
|
||||
public string? Kind { get; init; } // определил Storage: image/video/audio/document/archive/other
|
||||
public string? MimeType { get; init; }
|
||||
public string? FileName { get; init; }
|
||||
public long? Size { get; init; }
|
||||
public int? Width { get; init; }
|
||||
public int? Height { get; init; }
|
||||
public double? DurationSec { get; init; }
|
||||
public string? PreviewRef { get; init; } // превью/thumbnail
|
||||
public string? Caption { get; init; }
|
||||
public int? Order { get; init; }
|
||||
public IReadOnlyDictionary<string, string>? Meta { get; init; } // прочие метаданные от Storage
|
||||
}
|
||||
```
|
||||
|
||||
`ContactRef`: `Name?`, `Phone?`, `Email?`, `Url?`, `Kind?` (контакт может быть квалифицирован).
|
||||
|
||||
## 3. Storage-сервис (общий)
|
||||
|
||||
Отдельный сервис (как ai/ml/telegram), владелец — данные. Источники и ядро только ссылаются на объекты.
|
||||
|
||||
- **Загрузка:** `Upload(stream, token, fileName?) → DataRef`. Каждый сервис-источник выгружает свои данные
|
||||
**сам**, передавая **токен валидации** (сервисный токен/mTLS — уже есть в gRPC-обвязке).
|
||||
- **Определение типа:** Storage сам решает `Kind`/`MimeType`/размеры/длительность (контент-снифинг);
|
||||
контракт типы не задаёт.
|
||||
- **Чтение:** `Get(id) → (stream, DataRef)` либо выдача ссылки/временного URL.
|
||||
- **Бэкенд:** объектное хранилище (MinIO/S3). Путь/бакет — по тенанту.
|
||||
- **Владение:** единый общий сервис; каждый источник пишет в него со своим токеном, ядро/AI/ML читают по ссылке.
|
||||
|
||||
## 4. Адаптеры источников
|
||||
|
||||
```csharp
|
||||
public interface ISourceAdapter { string Kind { get; } SourceItem Normalize(object native); }
|
||||
```
|
||||
|
||||
Владельцы: `telegram` → telegram-сервис; `local` → ручное создание (Cards); `whatsapp`/`avito`/`web`/`file`/
|
||||
`excel` → соответствующий сервис. Файлы адаптер сам выгружает в Storage и кладёт в контракт `DataRef`.
|
||||
|
||||
## 5. Загрузка исходника карточки
|
||||
|
||||
Единый способ: по `SourceRef.Kind` — провайдер, возвращающий `SourceContent` (для файла — через Storage по
|
||||
`DataRef.Ref`, для сообщения — у источника). `ISourceContentProvider { Kind; LoadAsync(SourceRef) }` + реестр.
|
||||
API ядра: `GET /api/cards/{id}/source` → generic контент.
|
||||
|
||||
## 6. Персистентность
|
||||
|
||||
В карточках вместо плоских Telegram-колонок:
|
||||
|
||||
- `SourceKind` (text); `SourceJson` (jsonb, `SourceRef`);
|
||||
- `ContentJson` (jsonb, `SourceContent` — текст + `DataRef`-ссылки + прочее);
|
||||
- `SourceText` (text, FTS);
|
||||
- `SourceRefUrl` (text?, `OriginRef`).
|
||||
|
||||
Конвертер контента общий (без per-source сериализаторов). Миграции: старые удаляем → новый init с нуля.
|
||||
|
||||
## 7. Wire и фронт
|
||||
|
||||
- `CardDto.Source` = `{ kind, displayName?, originRef?, receivedAt }`.
|
||||
- `GET /api/cards/{id}/source` → `{ text?, html?, author?, subject?, data[], links[], contacts[], extra? }`.
|
||||
- Фронт: generic блок источника + универсальный просмотрщик (по `DataRef.Kind` — картинка/видео/аудио/файл;
|
||||
ссылки/контакты — списками).
|
||||
|
||||
## 8. Этапы
|
||||
|
||||
1. Домен: `SourceItem/SourceRef/SourceContent/DataRef/ContactRef`; удалить Telegram-маркеры из Cards.
|
||||
2. Storage-сервис: контракт gRPC, определение типа, токен валидации, бэкенд MinIO; регистрация.
|
||||
3. Персистентность: `SourceKind/SourceJson/ContentJson/SourceText/SourceRefUrl`, общий конвертер, новый init,
|
||||
маппинг KanbanStore.
|
||||
4. Pipeline: приём `SourceItem`, загрузка вложений в Storage адаптером, без Telegram-полей.
|
||||
5. Wire/API: generic `Source` в `CardDto`, `GET /api/cards/{id}/source`, провайдеры.
|
||||
6. Frontend: generic источник + универсальный просмотрщик.
|
||||
7. Telegram: адаптер + провайдер исходника (только в telegram-сервисе) + выгрузка в Storage.
|
||||
8. Комментарии: убрать упоминания Telegram из ядра и задачи/этапы — везде.
|
||||
|
||||
## 9. Реализация: зафиксированные сигнатуры
|
||||
|
||||
### Ядро: домен
|
||||
|
||||
- `SourceRefs` (Deal.Modules.Cards/Application/Sources): `Empty`, `DefaultHue = "#666"`,
|
||||
`HueKey = "hue"`, расширения `DedupeKey()` (вид|оригинал|внешний id), `ResolveHue()`.
|
||||
- `CardSnapshot`: вместо `ChannelName/ChannelHandle/ChannelHue/SourceMsg/SourceDialogId/SourceMsgId` —
|
||||
`SourceRef Source` + `SourceContent Content`; `ReceivedAt` остаётся.
|
||||
- `CardDto`: вместо `Channel`/`SourceMsg`/`SourceDialogId`/`SourceMsgId`/прежнего `Source` —
|
||||
`SourceRef Source` + `SourceContent Content`; `ReceivedAtMs` остаётся. `CardChannelDto`/`CardSourceDto` удалены.
|
||||
- `ICardStore.GetCardBySourceAsync(SourceRef source, CancellationToken ct)`.
|
||||
|
||||
### Ядро: конвейер
|
||||
|
||||
- `QueuedMessage { required SourceItem Item; bool Force; }`.
|
||||
- `QueueItemDto { string Id; SourceRef Source; SourceContent Content; string Text; string Status;
|
||||
long MsgAtMs; long QueuedAtMs; bool Force; }` (JsonIgnore на Force).
|
||||
- `RejectRecord { SourceRef Source; SourceContent Content; string Text; long MsgAtMs;
|
||||
string DecidedBy; string Stage; string Reason; string Kw; string? DeterministicId; }`
|
||||
(`DeterministicId = r_{Kind}_{OriginRef}_{ExternalId}`).
|
||||
- `RejectedItemDto`: `Source`/`Content`, `DecidedBy`/`DecidedByLabel` (решение), остальное как было.
|
||||
- `IPipelineStore.ExistsDuplicateAsync(SourceRef source, CancellationToken ct)`.
|
||||
- `PipelineChannelDto` удалён.
|
||||
|
||||
### Схема БД (схема тенанта)
|
||||
|
||||
- Cards: удалить `ChannelName/ChannelHandle/ChannelHue/SourceMsg/SourceDialogId/SourceMsgId`;
|
||||
добавить `SourceKind`, `SourceExternalId`, `SourceOriginRef` (text, для запросов), `SourceJson` (text),
|
||||
`ContentJson` (text), `SourceText` (text). FTS: Title+Summary+SourceText+Contact.
|
||||
- QueueItems: удалить `DialogId/ChannelName/ChannelHandle/ChannelHue/MsgId`; добавить
|
||||
`SourceKey` (text, уникальный ключ дедупа), `SourceJson`, `ContentJson`. `Text/MsgAt/Status/Force/CreatedAt/UpdatedAt` остаются.
|
||||
- RejectedItems: удалить `DialogId/MsgId/ChannelName/ChannelHandle/ChannelHue`; добавить
|
||||
`SourceKey`, `SourceJson`, `ContentJson`. FTS — по `Text`.
|
||||
- Миграции tenant: старые удалить, сгенерировать новый init с нуля (данных нет).
|
||||
|
||||
### Маппинг источника
|
||||
|
||||
- Telegram-адаптер (в ядре — тонкий край приёма): `Kind="telegram"`, `ExternalId=MsgId`,
|
||||
`OriginRef=DialogId`, `DisplayName=ChannelName`, `Extra["hue"]=ChannelHue` (иначе дефолт),
|
||||
`ReceivedAt=msgAt`, `Content.Text=Text`, `Content.Author=ChannelName`.
|
||||
- Дашборды/карточки/конвейер работают только с `SourceRef`/`SourceContent`; Telegram-поля не проходят дальше адаптера.
|
||||
|
||||
### Входящий поток (generic, 2026-09-11)
|
||||
|
||||
- `src/contracts/sources.proto` → сервис `SourceIngressService.PushSource` с generic-типами
|
||||
`SourceRefProto`/`SourceContentProto`/`DataRefProto`/`ContactRefProto`; вложения — ссылки на Storage.
|
||||
- Ядро: `Deal.Api/Sources/SourceIngressGrpcService` (приём) + `SourceProtoMapper` (proto → домен) +
|
||||
`ISourceIngestObserver` (вторичная обработка принятой записи, сбой наблюдателя не влияет на приём) +
|
||||
`IngressTenantResolver` (тенант по metadata).
|
||||
- Из `telegram.proto` удалён `IngressService.PushMessage` (остались `SyncDialogs`/`ReportStatus`);
|
||||
telegram-сервис шлёт записи через `PushSource` (`kind="telegram"`). Превью каталога/TgMessages сохраняет
|
||||
`TelegramSourceIngestObserver` (ядро, telegram-модуль — единственное место с telegram-спецификой приёма).
|
||||
- Любой другой источник (whatsapp/avito/файл/excel) шлёт тот же `PushSource` со своим `source.kind`.
|
||||
|
||||
### Remote-просмотр исходника (2026-09-11)
|
||||
|
||||
- `TelegramService.ReadSource(ReadSourceRequest{dialog_id, msg_id})` → `ReadSourceReply{found, text?, time?}`
|
||||
(`src/contracts/telegram.proto`); telegram-сервис достаёт конкретное сообщение
|
||||
(`ISessionClient.GetMessageAsync` → TL `Messages_GetMessages`). Медиа без текста → `found=false`.
|
||||
- Ядро: `ITelegramGateway.ReadSourceAsync` + `TelegramSourceContentProvider` (`ISourceContentProvider`,
|
||||
`Kind="telegram"`, `Deal.Infrastructure/Integrations/Sources`) — резолвится `SourceContentResolver`.
|
||||
- `GET /api/cards/{id}/source` отдаёт результат провайдера либо сохранённое содержимое карточки.
|
||||
Фронт: кнопка «Обновить из источника» в подробной карточке (`CardDrawer.vue` → `loadCardSource`).
|
||||
|
||||
@@ -1,53 +1,53 @@
|
||||
# Дизайн: разбиение проектов на логические папки (namespace = папка)
|
||||
|
||||
Дата: 2026-09-11. Статус: согласовано владельцем (решения 1–5).
|
||||
|
||||
## 1. Цель
|
||||
|
||||
Упорядочить код по назначению: вместо «свалки» файлов разных видов в одной папке с единым
|
||||
`namespace` — подпапки по назначению, при этом `namespace` соответствует пути папки.
|
||||
|
||||
## 2. Таксономия папок (по назначению)
|
||||
|
||||
| Папка | Что кладём |
|
||||
| --- | --- |
|
||||
| `Abstractions/` | интерфейсы `I*.cs` |
|
||||
| `Services/` | прикладная логика: `*Service`, `*WorkerService.*`, `*Guard`, `*Pacer`, `*Evaluator`, `*Counter`, `*Detector`, `*Normalizer`, `*Matcher`, `*Composer`, `*Cleaner`, `*Classifier`, `*Mapper`, `*Builder`, `*Writer`, `*Recomputer`, `*Suggester`, `*Filler`, `*Generator`, `*Hasher` и аналогичные исполнители |
|
||||
| `Models/` | доменные типы: сущности, value-объекты, enum, статусы/виды, константные реестры (`*Statuses`, `*Kinds`, `*Prefixes`, `*Keys`, `*Events`, `*Periods`, `*Sources`, `*Field`, `*Defaults`) |
|
||||
| `Dtos/` | транспортные типы: `*Dto`, `*Request`, `*Response`, `*Patch` |
|
||||
| `Extensions/` | `*Extensions` |
|
||||
| `Options/` | `*Options` |
|
||||
| `Exceptions/` | `*Exception` |
|
||||
| `Registrars/` | `*ModuleRegistrar` |
|
||||
|
||||
Существующие feature-папки (`ColumnRules`, `Parse`, существующие `Models`) сохраняются.
|
||||
|
||||
## 3. Правила
|
||||
|
||||
1. `namespace` строго соответствует пути папки.
|
||||
2. Имена типов и публичные контракты не меняются — только расположение и `namespace`.
|
||||
3. Один тип = один файл (уже соблюдается).
|
||||
4. Частичные классы (`Foo.cs`, `Foo.Part.cs`) переносятся вместе.
|
||||
5. Тестовые проекты группируются по областям: `Modules/<X>`, `Api`, `Infrastructure` и т.п.,
|
||||
`namespace` = `Deal.Tests.Unit.<Область>`.
|
||||
|
||||
## 4. Механика переноса (на проект)
|
||||
|
||||
1. Классифицировать файлы по таблице §2.
|
||||
2. Перенести файлы в подпапки и заменить `namespace`.
|
||||
3. Миграция `using`: в файлах-потребителях заменить несуществующий старый `using <OldNs>;` на
|
||||
`using` всех новых подпространств (пере-добавление безопасно; при коллизии имён — ручное разрешение).
|
||||
Файлы внутри проекта-источника получают `using` соседних подпространств.
|
||||
4. `dotnet build` → исправить остатки (полные имена, `cref`), `dotnet test`.
|
||||
5. Отдельный коммит (русский) после каждого проекта.
|
||||
|
||||
## 5. Порядок
|
||||
|
||||
Пилот — `Deal.Modules.Cards` (чистый домен). Далее: остальные `Deal.Modules.*`, затем
|
||||
`Deal.Infrastructure`, `Deal.Api`, `Deal.Contracts`/`Deal.SharedKernel`, сервисы `telegram/ai/ml`,
|
||||
затем тестовые проекты. После каждого шага — сборка + тесты + коммит.
|
||||
|
||||
## 6. Риски
|
||||
|
||||
- Коллизия простых имён при пере-добавлении `using` → разрешается вручную по ошибкам сборки.
|
||||
- Не забыть `cref`/полные имена в XML-док и `nameof` — выявляются сборкой.
|
||||
# Дизайн: разбиение проектов на логические папки (namespace = папка)
|
||||
|
||||
Дата: 2026-09-11. Статус: согласовано владельцем (решения 1–5).
|
||||
|
||||
## 1. Цель
|
||||
|
||||
Упорядочить код по назначению: вместо «свалки» файлов разных видов в одной папке с единым
|
||||
`namespace` — подпапки по назначению, при этом `namespace` соответствует пути папки.
|
||||
|
||||
## 2. Таксономия папок (по назначению)
|
||||
|
||||
| Папка | Что кладём |
|
||||
| --- | --- |
|
||||
| `Abstractions/` | интерфейсы `I*.cs` |
|
||||
| `Services/` | прикладная логика: `*Service`, `*WorkerService.*`, `*Guard`, `*Pacer`, `*Evaluator`, `*Counter`, `*Detector`, `*Normalizer`, `*Matcher`, `*Composer`, `*Cleaner`, `*Classifier`, `*Mapper`, `*Builder`, `*Writer`, `*Recomputer`, `*Suggester`, `*Filler`, `*Generator`, `*Hasher` и аналогичные исполнители |
|
||||
| `Models/` | доменные типы: сущности, value-объекты, enum, статусы/виды, константные реестры (`*Statuses`, `*Kinds`, `*Prefixes`, `*Keys`, `*Events`, `*Periods`, `*Sources`, `*Field`, `*Defaults`) |
|
||||
| `Dtos/` | транспортные типы: `*Dto`, `*Request`, `*Response`, `*Patch` |
|
||||
| `Extensions/` | `*Extensions` |
|
||||
| `Options/` | `*Options` |
|
||||
| `Exceptions/` | `*Exception` |
|
||||
| `Registrars/` | `*ModuleRegistrar` |
|
||||
|
||||
Существующие feature-папки (`ColumnRules`, `Parse`, существующие `Models`) сохраняются.
|
||||
|
||||
## 3. Правила
|
||||
|
||||
1. `namespace` строго соответствует пути папки.
|
||||
2. Имена типов и публичные контракты не меняются — только расположение и `namespace`.
|
||||
3. Один тип = один файл (уже соблюдается).
|
||||
4. Частичные классы (`Foo.cs`, `Foo.Part.cs`) переносятся вместе.
|
||||
5. Тестовые проекты группируются по областям: `Modules/<X>`, `Api`, `Infrastructure` и т.п.,
|
||||
`namespace` = `Deal.Tests.Unit.<Область>`.
|
||||
|
||||
## 4. Механика переноса (на проект)
|
||||
|
||||
1. Классифицировать файлы по таблице §2.
|
||||
2. Перенести файлы в подпапки и заменить `namespace`.
|
||||
3. Миграция `using`: в файлах-потребителях заменить несуществующий старый `using <OldNs>;` на
|
||||
`using` всех новых подпространств (пере-добавление безопасно; при коллизии имён — ручное разрешение).
|
||||
Файлы внутри проекта-источника получают `using` соседних подпространств.
|
||||
4. `dotnet build` → исправить остатки (полные имена, `cref`), `dotnet test`.
|
||||
5. Отдельный коммит (русский) после каждого проекта.
|
||||
|
||||
## 5. Порядок
|
||||
|
||||
Пилот — `Deal.Modules.Cards` (чистый домен). Далее: остальные `Deal.Modules.*`, затем
|
||||
`Deal.Infrastructure`, `Deal.Api`, `Deal.Contracts`/`Deal.SharedKernel`, сервисы `telegram/ai/ml`,
|
||||
затем тестовые проекты. После каждого шага — сборка + тесты + коммит.
|
||||
|
||||
## 6. Риски
|
||||
|
||||
- Коллизия простых имён при пере-добавлении `using` → разрешается вручную по ошибкам сборки.
|
||||
- Не забыть `cref`/полные имена в XML-док и `nameof` — выявляются сборкой.
|
||||
|
||||
Reference in New Issue
Block a user