using Deal.Api.Http; using Deal.Infrastructure.Services; using Deal.Api.Services; using Deal.Api.Dtos; namespace Deal.Api.Endpoints; /// /// Служебные storage-эндпоинты: POST /api/admin/tick и POST /api/admin/fts/rebuild (план Tasks 10–11, /// Rulings 6/8/11; прототип dashboard_routes.py L261–264, L327–337). /// /// /// Контракт 1:1 с прототипом и api-map §3.2 L103–112: POST /admin/tick = тик правил хранения текущего /// тенанта + очистка отсева пайплайна (3 суток) + проверка напоминаний «Отложено» (план Task 11, Ruling 3/8) /// + один проход pump очереди входящих (этап 4, Ruling 8/9); /// ответ {storage, reminders, pipeline: {…}, queue: N} (dashboard_routes.py L327–337; storage.purgedRejected /// объединяет очистку отсева — Ruling 9; reminders — «выстрелившие» напоминания {id,title,stage}, пусто — /// сработавших нет). SSE-публикации (тосты статистики notify_tick_stats L496–504, new_card по созданным /// карточкам и reminder_due по «выстрелившим» напоминаниям) выполняет из /// Api-слоя — модули остаются чистыми (Ruling 5/8); без подписчиков публикация — no-op. Сбой проверки /// напоминаний/pump не роняет тик: reminders/pipeline ответа пусты, очередь ждёт следующего тика/фонового /// цикла (Task 11). POST /admin/fts/rebuild — /// реальная идемпотентная пересборка FTS-индексов (CREATE INDEX IF NOT EXISTS + /// ANALYZE, Ruling 6), ответ {ok:true, ready:true} (при сбое {ok:false, ready:false} — 1:1 с fts_rebuild L261–264, /// кнопка Settings «Пересобрать индекс» store.js L1883–1889). Оба эндпоинта требуют сессию: 401 {detail} без /// куки (Ruling 10); сервисы резолвятся из RequestServices ПОСЛЕ проверки сессии (паттерн BoardsEndpoints). /// public static class StorageEndpoints { // Префикс группы (роутер dashboard, prefix="/api"; admin-пути прототипа L261/L327). private const string AdminGroupPrefix = "/api"; // Путь ручного тика правил хранения (dashboard_routes.py L327). private const string TickPath = "/admin/tick"; // Путь пересборки поискового индекса (dashboard_routes.py L261; Ruling 6). private const string FtsRebuildPath = "/admin/fts/rebuild"; // OpenAPI-тег группы (в прототипе роутер dashboard — dashboard_routes.py). private const string OpenApiTag = "dashboard"; /// /// Регистрирует POST /api/admin/tick и POST /api/admin/fts/rebuild. /// /// Построитель маршрутов приложения. /// Построитель маршрутов для цепочки вызовов. public static IEndpointRouteBuilder MapStorageEndpoints(this IEndpointRouteBuilder app) { var admin = app.MapGroup(AdminGroupPrefix).WithTags(OpenApiTag); admin.MapPost(TickPath, AdminTickAsync); admin.MapPost(FtsRebuildPath, FtsRebuildAsync); return app; } // POST /api/admin/tick: правила хранения + очистка отсева + напоминания + pump + SSE-тосты/new_card/reminder_due (admin_tick L327–337). // Весь состав тика — AdminTickOrchestrator (вынесен из эндпоинта для unit-тестов логики и // переиспользования): тик StorageTickService (Kanban) → PurgeExpiredAsync (отсев 3 суток, merge в // storage.purgedRejected) → тосты статистики (включая «Отсев очищен: N записей (3 дн.)») → CheckDueAsync // (напоминания «Отложено»: reminders ответа + SSE reminder_due, план Task 11; сбой не роняет тик) → // PumpOnceAsync (сбой не роняет тик) → SSE new_card по созданным карточкам → queue. Формы — 1:1 с прототипом. private static async Task AdminTickAsync(HttpContext context, CancellationToken ct) { if (!context.HasUser()) { return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail); } AdminTickOrchestrator orchestrator = context.RequestServices.GetRequiredService(); Guid tenantId = context.GetCurrentUser()!.TenantId; return Results.Ok(await orchestrator.TickAsync(tenantId, ct)); } // POST /api/admin/fts/rebuild: пересборка FTS-индексов тенанта; ответ {ok:true, ready:true} (Ruling 6). // SearchTsv — генерируемые STORED-колонки Cards/RejectedItems: авто-актуальны, «пересборка» = создание // отсутствующих GIN-индексов (CREATE INDEX IF NOT EXISTS) + ANALYZE таблиц (FtsMaintenance.RebuildAsync). // Сбой обслуживания возвращает {ok:false, ready:false} (прототип fts_rebuild L261–264: rebuild() → ok, // is_ready() → ready) — кнопка Settings фронта показывает ошибку по ready (store.js L1883–1889). private static async Task FtsRebuildAsync(HttpContext context, CancellationToken ct) { if (!context.HasUser()) { return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail); } FtsMaintenance fts = context.RequestServices.GetRequiredService(); bool ok = await fts.RebuildAsync(ct); return Results.Ok(new { ok, ready = ok }); } }