Table of Contents
- Дейл (Deal) — Этап 2: Настройки тенанта (Settings) Implementation Plan
- Global Constraints
- Зафиксированные решения (Rulings этапа)
- Задачи
- Task 1: Шифрование секретов (AES-GCM) — фундамент хранения ключей AI/Telegram
- Task 2: Модуль Settings — каталог ключей, дефолты, DTO, порт хранилища
- Task 3: SettingsService — public-снимок и частичное обновление (PATCH-семантика 1:1)
- Task 4: KV-адаптер SettingsStore (EF) и DI
- Task 5: Эндпоинты GET/PATCH /api/settings + DI + curl-приёмка
- Task 6: ИИ-провайдеры и POST /api/ai/check (проверка подключения)
- Task 7: Промпты и «Мои промпты» — интеграционная проверка границы с фронтом
- Task 8: Курсы валют — сервис, кэш, эндпоинты /api/rates*
- Task 9: ML-панель — порт IMlClient, детерминированная заглушка, эндпоинты /api/ml
- Task 10: Тестер фильтров — этап-1 правила и POST /api/admin/check-message
- Task 11: Финал этапа — интеграция и сквозная приёмка
- Self-Review
Перенесено из репозитория (
docs/superpowers/plans/2026-09-05-deal-stage2-settings.md). Актуальная версия — здесь, в вики.
Дейл (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-Postgresdeal-postgres(:5433). - Ответы: camelCase JSON; ошибки — HTTP +
{"detail"}; «мягкие» ошибки (ml/reset) — HTTP 200 с полемerror. - Дефолтные значения настроек/промптов — из констант прототипа
constants.pyиdata.js(фронт — высший авторитет форм; тексты промптов копируются изdata.jsL94–141).
Зафиксированные решения (Rulings этапа)
- Ruling 1 (модель настроек): типизированные ключи в существующей таблице
settings(keytext 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.pyL110–185). - Ruling 2 (шифрование секретов): AES-256-GCM (
System.Security.Cryptography.AesGcm), nonce 12 байт, tag 16 байт. Ключ — envDEAL_ENCRYPTION_KEY(32 байта, urlsafe-Base64); при отсутствии в dev — файл<ContentRoot>/data/encryption.key(генерируется при первом старте, лог-warning; путь переопределяется envDEAL_ENCRYPTION_KEY_FILE). Формат значения в БД:enc:+ Base64(nonce‖ct‖tag). Расшифровка повреждённого/чужого значения → пустая строка- warning (совместимость
crypto.decrypt_text,crypto.pyL52–61). ПортISecretCipher— в модуле Settings, адаптерAesGcmSecretCipher— в Infrastructure.
- warning (совместимость
- Ruling 3 (маски и публичная форма): маска
mask(v): пусто →"",len≤8→ как есть, иначеv[:4]+"…"+v[-4:](settings_routes.pyL28–32).aiConfigsнаружу —{id: {baseUrl, model, keySet, keyMasked}};tgKeys—{apiId: <маска>, apiHashSet: bool}. Списокproviders— статический из модуля (id,name,base,local,models; зеркалоconstants.AI_PROVIDERSL170–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.pyL20);USDT=USD(rates.pyL86–91). Mock-курсы — константаMockRates(constants.pyL41–50). Обновление: лениво на GET при протухании/смене источника, синхронно наPOST /rates/refresh, фоново-запуск на PATCHrateSource(settings_routes.pyL186–192). Массовый пересчёт карточек (recompute_conversions) — этап 3 (таблицы leads нет); в этапе 2 — только чистыйConvertAmount. - Ruling 7 (проверка AI): реальный HTTP, без LLM-вызовов, 1:1
settings_routes.pyL195–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.pyL36–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.jsL106–111, в PATCH не шлются) — бэкенд не нужен. Общие напоминания — ключremindersEnabled(passthrough); отложенные напоминания иreminder_due— этап 5 (Projects). - Ruling 11 (приёмка и фронт): Vue
boot()(store.jsL571–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(envDEAL_ENCRYPTION_KEY_FILE), генерация при первом старте + warning; невалидный env-ключ → исключение при старте (семантикаcrypto._get_fernet,crypto.pyL22–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.pyL189–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.jsL94–141 (фронт — источник; вconstants.pyL63–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.pyL170–186). - Create:
S/Application/MockRates.cs(константа,constants.pyL41–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: нечисловое → пропуск ключа; клампы:
archiveAfterDays1..30,minLen10..500,discJoinLimit1..200,discJoinDelayMin/Max5..600,discEvalSample3..30,discEvalThreshold1..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):
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.PATCH {"archiveAfterDays":99,"minLen":3,"discJoinDelayMin":700,"discJoinDelayMax":5}→ в ответеarchiveAfterDays:30, minLen:10, discJoinDelayMin:5, discJoinDelayMax:700(swap).PATCH {"myPrompts":[{name:"x",prompt:"y"},{name:"",prompt:""}]}→ 1 элемент,idначинаетсяpp_.PATCH {"aiConfigs":{"deepseek":{"apiKey":"sk-1234567890ab"}}}→ ответkeySet:true, keyMasked:"sk-1…90ab"; psql:value_jsonсодержитenc:(см. Task 7-контракт psql).PATCH {"tgKeys":{"apiId":"123456","apiHash":"abcdefghijklmnop"}}→apiHashSet:true.- Неизвестный ключ
{"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(fakeHttpMessageHandler): без ключа; 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_promptL63–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.pyL77–83);ConvertAmount(amount, fromCur, toCur)— USDT→USD (L86–103). Ленивое обновление на GET приShouldFetch— фоновый запускRefreshAsync, ответ — текущий кэш. - Create:
I/Integrations/CbrRateSource.cs— HTTP GEThttps://www.cbr-xml-daily.ru/daily_json.js(JSON),Valute[code].Value/Nominal,RUB:1; сбой → null (лог) (rates.pyL43–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:1ml_routes.pyL70–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— DIIMlClient→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.pyL94–124) поверхISettingsStore: минимальная длина (minLen), стоп-фразы (casefold, ответ — конкретная фраза), блокировка резюме (blockResumes+resumeMarkersс контекстным guard «вакансия… присылайте резюме» — не режем,pipeline._resume_reasonL644–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:1dashboard_routes.pyL267–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.sln0/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(раздел настроек: envDEAL_ENCRYPTION_KEY, поведение GET/PATCH /settings, креды). - Отчёт
task-11-report.md+ финальная строка вprogress.md.
Self-Review
- 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.
- Placeholder scan: конкретные адаптеры и контракты; «заглушки» только там, где разрешено
решением владельца (п.5):
LocalMlClient(Task 9), ИИ-фильтр в тестере = skipped (Task 10); референсы на строки файлов точные. FIXME/TODO нет. - Type consistency: один модуль Settings владеет каталогом ключей/дефолтами — Kanban/Pipeline
(этапы 3/4) читают те же ключи через
ISettingsStore;IMlClient-контракт (Contracts) един для панели (этап 2), счётчиков (этап 3) и предсказаний (этап 4); сущностьTenantSettingEntityне меняется; схемы/миграции не добавляются. - Вне scope этапа 2: канбан-колонки/карточки/архив-корзина и их эндпоинты (этап 3), pipeline/очередь/отсев/дедуп (этап 4), projects/напоминания-отложенные/файлы (этап 5), реальные ml/ai/telegram-сервисы и /api/tg/* (этап 6), discovery, оператор/инвайты/лимиты/ аудит (этап 7); colState-эндпоинты; библиотека промптов (фронтовая); звук/вид (фронт).