Обновить доки по закрытию остатков код-стайла
Аудит §2 переписан под решения (var-гейт, LF, дедуп закрыт — дублей нет); backlog: TD-COMMENTS-IFACE п.1–3 закрыты, TD-STYLE-ANALYZERS закрыт; STATUS.md — новый блок захода, устаревший блок «Осталось (в backlog)» в шапке удалён; план и ledger захода.
This commit is contained in:
@@ -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` не прогонялись.
|
||||
Reference in New Issue
Block a user