feat: implement Blazor web interface for GM session management
Deploy Telegram Bot / deploy (push) Has been cancelled

- Created GmRelay.Web project (Blazor Server)
- Created GmRelay.Shared library for domain models and rendering
- Refactored GmRelay.Bot to use the Shared library
- Integrated Telegram Login widget with server-side HMAC verification
- Added Dashboard, Group Details, and Edit Session pages
- Enabled bot notifications and in-place message updates from web actions
- Updated .NET Aspire orchestration and Docker Compose configuration
This commit is contained in:
2026-04-17 11:06:59 +03:00
parent c27456e726
commit 988133e389
93 changed files with 61016 additions and 34 deletions
@@ -0,0 +1,62 @@
using GmRelay.Shared.Domain;
using Telegram.Bot.Types.ReplyMarkups;
namespace GmRelay.Shared.Rendering;
public sealed record SessionBatchDto(Guid SessionId, DateTime ScheduledAt, string Status);
public sealed record ParticipantBatchDto(Guid SessionId, string DisplayName, string? TelegramUsername);
public static class SessionBatchRenderer
{
public static (string Text, InlineKeyboardMarkup Markup) Render(
string title,
IReadOnlyList<SessionBatchDto> sessions,
IReadOnlyList<ParticipantBatchDto> participants)
{
var activeSessions = sessions.OrderBy(s => s.ScheduledAt).ToList();
var messageText = $"🎲 <b>Новые игры:</b> {System.Net.WebUtility.HtmlEncode(title)}\n\n" +
$"<b>Расписание:</b>\n\n";
var buttons = new List<InlineKeyboardButton[]>();
foreach (var session in activeSessions)
{
var sessionPlayers = participants.Where(p => p.SessionId == session.SessionId).ToList();
messageText += $"📅 <b>{session.ScheduledAt.FormatMoscow()}</b>\n";
messageText += $"👥 Игроки ({sessionPlayers.Count}):\n";
if (sessionPlayers.Count > 0)
{
messageText += string.Join("\n", sessionPlayers.Select(p => $" 👤 {(p.TelegramUsername != null ? "@" + p.TelegramUsername : p.DisplayName)}")) + "\n";
}
else
{
messageText += " <i>Пока никто не записался</i>\n";
}
if (session.Status == "Cancelled")
{
messageText += "❌ <i>Сессия отменена</i>\n\n";
}
else if (session.Status == "RecruitmentClosed")
{
messageText += "🔒 <i>Набор завершен</i>\n\n";
}
else
{
messageText += "\n";
var dateTitle = session.ScheduledAt.FormatMoscowShort();
buttons.Add(new[]
{
InlineKeyboardButton.WithCallbackData($"✋ На {dateTitle}", $"join_session:{session.SessionId}"),
InlineKeyboardButton.WithCallbackData($"❌ Отменить {dateTitle} (ГМ)", $"cancel_session:{session.SessionId}"),
InlineKeyboardButton.WithCallbackData($"⏰ (ГМ)", $"reschedule_session:{session.SessionId}")
});
}
}
return (messageText, new InlineKeyboardMarkup(buttons));
}
}