Инициализировать репозиторий «Дейл»

Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы
ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ,
инструкция пользователя, техдокументация, код-стайл), бэклог,
скрипты развёртывания и архив прототипа LeadRadar.
This commit is contained in:
Rustam Khalimov
2026-09-11 02:50:17 +03:00
commit 9e07568ddd
1402 changed files with 177470 additions and 0 deletions
@@ -0,0 +1,82 @@
using Deal.Modules.Kanban.Application.Models;
namespace Deal.Api.Events;
/// <summary>
/// Публикация SSE-тостов статистики тика правил хранения в канал тенанта (Ruling 8; notify_tick_stats L496504).
/// </summary>
/// <remarks>
/// Единый хелпер Api-слоя для POST /api/admin/tick (StorageEndpoints/AdminTickOrchestrator, Task 10) и
/// фонового StorageTickScheduler (Task 11): публикует тосты только по ненулевым счётчикам, тексты и иконки
/// 1:1 с прототипом; без подписчиков канала публикация — no-op (Ruling 5). Вынесен из StorageEndpoints,
/// чтобы ручной и фоновый тики не дублировали логику. Модуль Kanban тосты не публикует (Ruling 5:
/// публикации SSE — обязанность Api-слоя).
/// </remarks>
public sealed class StorageToastPublisher
{
// Тип SSE-события тоста (Ruling 5; api.js L79 слушает 'toast').
private const string ToastEventType = "toast";
// Текст тоста автоархива: N карточек ушло в архив (notify_tick_stats L498).
private const string AutoArchiveToastText = "Автоархив: {0} карточек";
// Текст тоста очистки архива: N карточек удалено из архива (notify_tick_stats L500).
private const string ArchiveClearedToastText = "Архив очищен: {0} (90 дн.)";
// Текст тоста очистки корзины: N карточек удалено из корзины (notify_tick_stats L502).
private const string TrashClearedToastText = "Корзина очищена: {0} (7 дн.)";
// Текст тоста автоочистки отсева пайплайна: N записей старше 3 суток (notify_tick_stats L503504).
private const string RejectedPurgedToastText = "Отсев очищен: {0} записей (3 дн.)";
// Иконка тоста автоархива (Ruling 8, 1:1 с прототипом).
private const string ClockIcon = "clock";
// Иконка тостов очисток архива/корзины (Ruling 8, 1:1 с прототипом).
private const string TrashIcon = "trash";
private readonly SseBroker _broker;
/// <summary>
/// Создаёт публикатора тостов тика.
/// </summary>
/// <param name="broker">SSE-брокер каналов тенантов (singleton).</param>
public StorageToastPublisher(SseBroker broker)
{
ArgumentNullException.ThrowIfNull(broker);
_broker = broker;
}
/// <summary>
/// Публикует тосты статистики тика по ненулевым счётчикам (notify_tick_stats L496504).
/// </summary>
/// <remarks>Тексты и иконки 1:1 с прототипом; дни в скобках («90 дн.»/«7 дн.»/«3 дн.») — фиксированные
/// строки прототипа (не пересчитываются от настроек). Ветка purgedRejected («Отсев очищен: N записей
/// (3 дн.)») — план Task 10: счётчик наполняет оркестратор тика (AdminTickOrchestrator) очисткой отсева
/// PipelineProcessingService.PurgeExpiredAsync; фоновый цикл Task 11 публикует ту же ветку по своему тику.
/// Публикация в канал тенанта; без подписчиков — no-op (Ruling 5).</remarks>
/// <param name="tenantId">Тенант-получатель тостов (сессия запроса / канал тенанта цикла).</param>
/// <param name="stats">Статистика только что выполненного тика.</param>
public void PublishTickToasts(Guid tenantId, StorageTickStatsDto stats)
{
if (stats.Archived > 0)
{
_broker.Publish(tenantId, ToastEventType, new { text = string.Format(AutoArchiveToastText, stats.Archived), icon = ClockIcon });
}
if (stats.PurgedArchive > 0)
{
_broker.Publish(tenantId, ToastEventType, new { text = string.Format(ArchiveClearedToastText, stats.PurgedArchive), icon = TrashIcon });
}
if (stats.PurgedTrash > 0)
{
_broker.Publish(tenantId, ToastEventType, new { text = string.Format(TrashClearedToastText, stats.PurgedTrash), icon = TrashIcon });
}
if (stats.PurgedRejected > 0)
{
_broker.Publish(tenantId, ToastEventType, new { text = string.Format(RejectedPurgedToastText, stats.PurgedRejected), icon = TrashIcon });
}
}
}