diff --git a/src/GmRelay.DiscordBot/Features/Sessions/DiscordListSessionsHandler.cs b/src/GmRelay.DiscordBot/Features/Sessions/DiscordListSessionsHandler.cs index 5c3ceed..276b8a3 100644 --- a/src/GmRelay.DiscordBot/Features/Sessions/DiscordListSessionsHandler.cs +++ b/src/GmRelay.DiscordBot/Features/Sessions/DiscordListSessionsHandler.cs @@ -41,7 +41,7 @@ public sealed class DiscordListSessionsHandler( WHERE g.platform = 'Discord' AND g.external_group_id = @GuildId AND s.status != @Cancelled - AND s.scheduled_at > NOW() + AND s.scheduled_at > now() - interval '4 hours' GROUP BY s.id, s.title, s.scheduled_at, s.status, s.max_players ORDER BY s.scheduled_at ASC", new diff --git a/src/GmRelay.DiscordBot/Features/Sessions/DiscordNewSessionCommand.cs b/src/GmRelay.DiscordBot/Features/Sessions/DiscordNewSessionCommand.cs index 1acfdf6..19e8af6 100644 --- a/src/GmRelay.DiscordBot/Features/Sessions/DiscordNewSessionCommand.cs +++ b/src/GmRelay.DiscordBot/Features/Sessions/DiscordNewSessionCommand.cs @@ -44,10 +44,12 @@ public class DiscordNewSessionCommand : ApplicationCommandModule HandleAsync( string guildId, string channelId, + string groupName, ulong userId, string userDisplayName, ulong resolvedPermissions, @@ -58,6 +59,9 @@ public sealed class DiscordNewSessionHandler( CancellationToken cancellationToken) { await using var connection = await dataSource.OpenConnectionAsync(cancellationToken); + var displayGroupName = string.IsNullOrWhiteSpace(groupName) || string.Equals(groupName, guildId, StringComparison.Ordinal) + ? title + : groupName.Trim(); var dbManagerUserIds = await connection.QueryAsync( @"SELECT CAST(p.external_user_id AS BIGINT) @@ -88,13 +92,13 @@ public sealed class DiscordNewSessionHandler( var groupId = await connection.ExecuteScalarAsync( @"INSERT INTO game_groups (name, platform, external_group_id, external_channel_id) - VALUES (@GuildId, 'Discord', @GuildId, @ChannelId) + VALUES (@GroupName, 'Discord', @GuildId, @ChannelId) ON CONFLICT (platform, external_group_id) WHERE platform IS NOT NULL AND external_group_id IS NOT NULL DO UPDATE SET name = EXCLUDED.name, external_channel_id = COALESCE(EXCLUDED.external_channel_id, game_groups.external_channel_id) RETURNING id", - new { GuildId = guildId, ChannelId = channelId }, + new { GroupName = displayGroupName, GuildId = guildId, ChannelId = channelId }, transaction); await connection.ExecuteAsync( diff --git a/src/GmRelay.Web/Components/Pages/Home.razor b/src/GmRelay.Web/Components/Pages/Home.razor index be859dd..d70130d 100644 --- a/src/GmRelay.Web/Components/Pages/Home.razor +++ b/src/GmRelay.Web/Components/Pages/Home.razor @@ -44,9 +44,14 @@
🎮

@group.Name

ID: @(group.Platform == "Discord" ? group.ExternalGroupId : group.TelegramChatId.ToString())

- - @FormatRole(group.ManagerRole) - +
+ + @FormatPlatform(group.Platform) + + + @FormatRole(group.ManagerRole) + +
Посмотреть игры → @@ -81,6 +86,20 @@ font-family: 'Courier New', monospace; margin-bottom: 1rem; } + + .group-card-meta { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: center; + margin-bottom: 1rem; + } + + .platform-badge { + background: rgba(88, 101, 242, 0.15); + color: #9ea8ff; + border-color: rgba(88, 101, 242, 0.35); + } @code { @@ -104,4 +123,7 @@ private static string FormatRole(string role) => GroupManagerRoleExtensions.FromDatabaseValue(role).ToDisplayName(); + + private static string FormatPlatform(string? platform) => + string.Equals(platform, "Discord", StringComparison.OrdinalIgnoreCase) ? "Discord" : "Telegram"; } diff --git a/src/GmRelay.Web/Services/SessionService.cs b/src/GmRelay.Web/Services/SessionService.cs index 212c065..4d0198c 100644 --- a/src/GmRelay.Web/Services/SessionService.cs +++ b/src/GmRelay.Web/Services/SessionService.cs @@ -123,11 +123,18 @@ public sealed class SessionService( SELECT g.id, g.telegram_chat_id AS TelegramChatId, g.external_group_id AS ExternalGroupId, - g.name, + COALESCE(NULLIF(g.name, g.external_group_id), latest_session.title, g.name) AS Name, g.platform AS Platform, vg.ManagerRole FROM visible_groups vg JOIN game_groups g ON g.id = vg.group_id + LEFT JOIN LATERAL ( + SELECT s.title + FROM sessions s + WHERE s.group_id = g.id + ORDER BY s.scheduled_at DESC + LIMIT 1 + ) latest_session ON true ORDER BY g.name """, new @@ -146,10 +153,17 @@ public sealed class SessionService( SELECT g.id, g.telegram_chat_id AS TelegramChatId, g.external_group_id AS ExternalGroupId, - g.name, + COALESCE(NULLIF(g.name, g.external_group_id), latest_session.title, g.name) AS Name, g.platform AS Platform, @OwnerRole AS ManagerRole FROM game_groups g + LEFT JOIN LATERAL ( + SELECT s.title + FROM sessions s + WHERE s.group_id = g.id + ORDER BY s.scheduled_at DESC + LIMIT 1 + ) latest_session ON true WHERE g.id = @GroupId """, new { GroupId = groupId, OwnerRole = GroupManagerRoleExtensions.OwnerValue }); diff --git a/tests/GmRelay.Bot.Tests/Discord/DiscordListSessionsHandlerTests.cs b/tests/GmRelay.Bot.Tests/Discord/DiscordListSessionsHandlerTests.cs index 2270513..86a275f 100644 --- a/tests/GmRelay.Bot.Tests/Discord/DiscordListSessionsHandlerTests.cs +++ b/tests/GmRelay.Bot.Tests/Discord/DiscordListSessionsHandlerTests.cs @@ -33,7 +33,17 @@ public sealed class DiscordListSessionsHandlerTests Assert.Contains("platform = 'Discord'", handler, StringComparison.Ordinal); Assert.Contains("external_group_id = @GuildId", handler, StringComparison.Ordinal); - Assert.Contains("scheduled_at > NOW()", handler, StringComparison.Ordinal); + Assert.Contains("scheduled_at > now() - interval '4 hours'", handler, StringComparison.Ordinal); + } + + [Fact] + public void Handler_ShouldIncludeRecentlyStartedSessionsForCleanup() + { + var repoRoot = GetRepoRoot(); + var handlerPath = Path.Combine(repoRoot, "src", "GmRelay.DiscordBot", "Features", "Sessions", "DiscordListSessionsHandler.cs"); + var handler = File.ReadAllText(handlerPath); + + Assert.Contains("now() - interval '4 hours'", handler, StringComparison.OrdinalIgnoreCase); } [Fact] diff --git a/tests/GmRelay.Bot.Tests/Discord/DiscordNewSessionHandlerTests.cs b/tests/GmRelay.Bot.Tests/Discord/DiscordNewSessionHandlerTests.cs index f0fbb05..6cae87f 100644 --- a/tests/GmRelay.Bot.Tests/Discord/DiscordNewSessionHandlerTests.cs +++ b/tests/GmRelay.Bot.Tests/Discord/DiscordNewSessionHandlerTests.cs @@ -171,6 +171,18 @@ public sealed class DiscordNewSessionHandlerTests Assert.DoesNotContain("PlatformScheduleMessage", source, StringComparison.Ordinal); } + [Fact] + public void Handler_ShouldStoreReadableDiscordGroupNameForWebCards() + { + var repoRoot = GetRepoRoot(); + var handlerPath = Path.Combine(repoRoot, "src", "GmRelay.DiscordBot", "Features", "Sessions", "DiscordNewSessionHandler.cs"); + var source = File.ReadAllText(handlerPath); + + Assert.Contains("groupName", source, StringComparison.Ordinal); + Assert.Contains("displayGroupName", source, StringComparison.Ordinal); + Assert.Contains("VALUES (@GroupName, 'Discord'", source, StringComparison.Ordinal); + } + private static DateTimeOffset FutureDateAt1930() { var future = DateTimeOffset.UtcNow.AddDays(7); diff --git a/tests/GmRelay.Bot.Tests/Web/HomePageSourceTests.cs b/tests/GmRelay.Bot.Tests/Web/HomePageSourceTests.cs new file mode 100644 index 0000000..2f81daa --- /dev/null +++ b/tests/GmRelay.Bot.Tests/Web/HomePageSourceTests.cs @@ -0,0 +1,35 @@ +namespace GmRelay.Bot.Tests.Web; + +public sealed class HomePageSourceTests +{ + [Fact] + public async Task HomePage_ShouldShowPlatformBadgeForGroups() + { + var source = await ReadRepositoryFileAsync("src/GmRelay.Web/Components/Pages/Home.razor"); + + Assert.Contains("platform-badge", source, StringComparison.Ordinal); + Assert.Contains("FormatPlatform", source, StringComparison.Ordinal); + Assert.Contains("group.Platform", source, StringComparison.Ordinal); + } + + [Fact] + public async Task SessionService_ShouldUseSessionTitleWhenDiscordGroupNameIsOnlyId() + { + var source = await ReadRepositoryFileAsync("src/GmRelay.Web/Services/SessionService.cs"); + + Assert.Contains("latest_session.title", source, StringComparison.Ordinal); + Assert.Contains("NULLIF(g.name, g.external_group_id)", source, StringComparison.Ordinal); + } + + private static async Task ReadRepositoryFileAsync(string relativePath) + { + var dir = AppContext.BaseDirectory; + while (!string.IsNullOrEmpty(dir) && !File.Exists(Path.Combine(dir, "Directory.Build.props"))) + { + dir = Directory.GetParent(dir)?.FullName; + } + + var repoRoot = dir ?? throw new InvalidOperationException("Could not find repo root"); + return await File.ReadAllTextAsync(Path.Combine(repoRoot, relativePath)); + } +}