docs(#22): обновить Архитектуру — neutral rendering, v1.10.0

2026-05-06 12:06:28 +03:00
parent 173b1bdcb1
commit b88b33955a
@@ -6,102 +6,52 @@ GM-Relay состоит из нескольких .NET-проектов и об
- `src/GmRelay.AppHost` — Aspire AppHost. Поднимает PostgreSQL с PgAdmin и запускает bot/web проекты.
- `src/GmRelay.Bot` — Worker Service с Telegram long polling, миграциями, планировщиком и вертикальными срезами бота.
- `src/GmRelay.Web` — Blazor Server UI для ГМа, включая Telegram Login Widget и Telegram Mini App вход.
- `src/GmRelay.Shared`общие доменные значения и rendering-код Telegram-сообщений.
- `src/GmRelay.ServiceDefaults` общие Aspire defaults: OpenTelemetry, service discovery, resilience, health checks.
- `src/GmRelay.Web` — Blazor Server UI для ГМа, включая Telegram Login Widget и Telegram Mini App.
- `src/GmRelay.Shared`чисто нейтральные доменные типы, DTO и platform-agnostic рендеринг. **НЕ зависит от Telegram.Bot**.
- `src/GmRelay.ServiceDefaults` — Aspire defaults: OpenTelemetry, service discovery, resilience, health checks.
## Архитектурное решение
## Вертикальная архитектура
В проекте принят подход Vertical Slice Architecture. Логика бота лежит в `src/GmRelay.Bot/Features/*`, где каждый сценарий содержит свой handler и локальные DTO.
Все use-cases живут в `src/GmRelay.Bot/Features/*/`. Каждый фича-срез содержит свои handler, DTO и тесты. Зависимости идут только к `Shared`. См. `docs/adr/0001-use-vertical-slice-native-aot-and-aspire.md`.
Причины такого выбора зафиксированы в `docs/adr/0001-use-vertical-slice-native-aot-and-aspire.md`:
## Telegram updates
- сценариев немного, отдельный Clean Architecture слой был бы лишним;
- зависимости Npgsql и Telegram.Bot стабильны;
- Native AOT требует явной регистрации зависимостей и аккуратного SQL-маппинга;
- BackgroundService + PeriodicTimer проще и надёжнее Quartz.NET для периодических проверок расписания и дедлайнов голосования.
`TelegramBotService` получает updates через long polling и маршрутизирует их в `UpdateRouter`.
## Поток Telegram updates
Команды и callback:
- `/start`, `/help`, `/newsession`, `/listsessions`, `/exportcalendar`
- `join_session:<sessionId>`, `leave_session:<sessionId>`, `cancel_session:<sessionId>`
- `reschedule_session:<sessionId>`, `reschedule_vote:<optionId>`, `rsvp:<confirm|decline>:<sessionId>`
`TelegramBotService` получает updates через long polling и передаёт их в `UpdateRouter`.
## Рендеринг батчей (v1.10.0+)
`UpdateRouter` явно маршрутизирует:
Рендеринг разделён на platform-neutral View Builder и platform-specific Renderers:
- `/start`, `/help`, `/newsession`, `/listsessions`, `/exportcalendar`;
- callback `join_session:<sessionId>`;
- callback `leave_session:<sessionId>`;
- callback `cancel_session:<sessionId>`;
- callback `delete_session:<sessionId>`;
- callback `reschedule_session:<sessionId>`;
- callback `reschedule_vote:<optionId>`;
- callback `rsvp:<confirm|decline>:<sessionId>`;
- обычные текстовые сообщения ГМа как ввод 2-3 вариантов времени и дедлайна для активного переноса.
- `SessionBatchViewBuilder` (`GmRelay.Shared.Rendering`) — собирает нейтральную view model (`SessionBatchViewModel`) из доменных DTO. Не зависит от Telegram.
- `TelegramSessionBatchRenderer` (`GmRelay.Bot.Infrastructure.Telegram` / `GmRelay.Web.Services`) — рендерит HTML + `InlineKeyboardMarkup` для Telegram.
- `DiscordSessionBatchRenderer` (`GmRelay.Shared.Rendering`) — заглушка для Discord renderer (issue #26).
- `BatchMessageEditor` (`GmRelay.Bot` / `GmRelay.Web`) — редактирует batch-сообщения в Telegram (текст или caption).
`/newsession` может прийти как обычный text message или как caption к прикреплённому фото. Вариант с caption используется для обложек batch-поста: `CreateSessionHandler` отправляет фото отдельным сообщением перед текстовым расписанием, чтобы обновления состава продолжали редактировать обычный `batch_message_id`.
Маршрутизация не использует reflection или assembly scanning.
## Планировщик
`SessionSchedulerService` — stateless background service. Он запускается сразу при старте, затем раз в минуту:
1. Ищет сессии `Planned`, у которых `scheduled_at - 24h <= now()`.
2. Вызывает `SendConfirmationHandler` и переводит сессию в `ConfirmationSent`.
3. Ищет сессии `Confirmed` или `ConfirmationSent`, у которых `scheduled_at - 1h <= now()` и `one_hour_reminder_processed_at IS NULL`.
4. Вызывает `SendOneHourReminderHandler`, который учитывает `sessions.notification_mode`.
5. Ищет сессии `Confirmed`, у которых `scheduled_at - 5min <= now()` и `link_message_id IS NULL`.
6. Вызывает `SendJoinLinkHandler`.
Состояние хранится в PostgreSQL, поэтому после рестарта сервис продолжает работу без in-memory очередей.
`RescheduleVotingDeadlineService` — отдельный stateless background service. Он раз в минуту ищет `reschedule_proposals` в статусе `Voting`, у которых `voting_deadline_at <= now()`, выбирает вариант с наибольшим числом голосов, применяет перенос или отклоняет голосование при ничьей/нуле голосов, обновляет Telegram-сообщения и сбрасывает RSVP при успешном переносе.
Цепочка использования:
```csharp
var view = SessionBatchViewBuilder.Build(title, sessions, participants);
var (text, markup) = TelegramSessionBatchRenderer.Render(view);
```
## Web UI
Blazor Server-приложение использует cookie auth и Telegram Login Widget.
Начиная с `1.9.0`, тот же Web UI открывается как Telegram Mini App через `/miniapp`; в `1.9.9` основной вход по-прежнему проверяет Telegram `initData`, а fallback `/login` использует callback-mode Telegram Login Widget и endpoint `/auth/telegram-login`, чтобы cookie выставлялась внутри активного Mini App WebView без ручного закрытия и повторного открытия.
Blazor Server-side с cookie auth и Telegram Login Widget.
Основные сервисы:
Ключевые endpoints:
- `/login` — Telegram Login Widget
- `/miniapp` — Telegram Mini App WebView
- `/auth/telegram-login` — callback endpoint для Telegram Login Widget
- `/auth/status` — проверка статуса аутентификации
- `/` — дашборд (только owner и co-GM)
- `/templates` — шаблоны кампаний
- `/group/{GroupId}` — сессии группы, bulk-операции
- `/session/edit/{SessionId}` — редактирование сессии
- `TelegramAuthService` валидирует HMAC-SHA256 подпись Telegram Login Widget, Telegram WebApp `initData` и `auth_date`.
- `SessionService` читает и обновляет группы/сессии через Dapper, включая шаблоны кампаний и bulk-операции по `batch_id`.
- `SessionService` сохраняет режим уведомлений batch и дублирует Web-уведомления в ЛС, если включён `GroupAndDirect`.
- `SessionService` читает `group_managers`, проверяет роли `Owner`/`CoGm` и сохраняет назначения co-GM.
- `BatchSchedulePlanner` задаёт детерминированные правила fixed-interval переноса, повторяющихся расписаний и clone-смещения batch.
- `AuthorizedSessionService` проверяет, что текущий Telegram ID является owner или co-GM группы; owner-only операции делегирования проходят через отдельную проверку роли.
## C4-диаграммы
Основные страницы:
- `/login` — вход через Telegram.
- `/miniapp` — вход из Telegram Mini App через серверную проверку `initData`.
- `/auth/telegram-login` — callback-вход Telegram Login Widget: принимает JSON payload, проверяет Telegram HMAC и выдаёт cookie-сессию внутри текущего WebView.
- `/auth/status` — лёгкая проверка текущей cookie-сессии для Mini App fallback-flow.
- `/` — список групп, где пользователь owner или co-GM.
- `/templates` — отдельная вкладка управления шаблонами кампаний по доступным группам.
- `/group/{GroupId}` — управление группой, применение шаблонов кампаний, будущие сессии и batch bulk-операции.
- `/session/edit/{SessionId}` — редактирование названия, времени и ссылки.
Поток Mini App:
1. Бот открывает URL `Telegram:MiniAppUrl` через menu button или inline WebApp-кнопку `/start`.
2. Страница `/miniapp` ждёт `Telegram.WebApp.initData` из Telegram JS API.
3. Blazor отправляет initData на `/auth/telegram-webapp`.
4. Если `initData` недоступен, fallback `/login` регистрирует `data-onauth`, получает Telegram Login Widget payload в JavaScript и отправляет его на `/auth/telegram-login` без внешнего redirect-flow.
5. Shell вызывает `Telegram.WebApp.ready()` / `expand()`, синхронизирует `safeAreaInset` и `contentSafeAreaInset` в CSS-переменные и слушает `safeAreaChanged`, `contentSafeAreaChanged`, `viewportChanged`.
6. Сервер проверяет подпись через ключ `WebAppData` + токен бота, извлекает Telegram ID из `user` и создаёт стандартную auth-cookie.
7. Дальше пользователь работает с теми же страницами и сервисами, что и в обычном Web Dashboard.
## Общий домен
`GmRelay.Shared` содержит:
- `SessionStatus`: `Planned`, `ConfirmationSent`, `Confirmed`, `Cancelled`.
- `RsvpStatus`: `Pending`, `Confirmed`, `Declined`.
- `SessionNotificationMode`: `GroupAndDirect`, `GroupOnly`.
- `GroupManagerRole`: `Owner`, `CoGm`.
- `MoscowTime`: парсинг и форматирование МСК (`UTC+3`).
- `SessionBatchRenderer`: единый renderer для Telegram-сообщения пачки сессий.
## Диаграммы
C4-диаграммы хранятся в `docs/c4-system-context.md`. Там описаны system context, container view и component view для `GmRelay.Bot`.
Хранятся в `docs/c4-system-context.md`. Описывают system context, container view и component view для `GmRelay.Bot`.