feat(web): dashboard session deletion and E2E coverage for issue #150
Deploy Telegram Bot / build-and-push (push) Failing after 28m28s
Deploy Telegram Bot / scan-images (push) Has been skipped
Deploy Telegram Bot / deploy (push) Has been skipped

- Add DeleteSessionAsync to ISessionStore/SessionService (unpublish portfolio card,
  remove bot-created empty forum topic, update batch message).
- Add DeleteSessionForCurrentUserAsync to AuthorizedSessionService with audit log.
- Add delete button + confirmation dialog to GroupDetails.razor.
- Extend dashboard Playwright tests with edit persistence and delete verification.
- Update AuthorizedSessionServiceTests with delete authorization coverage.
- Mark issue #150 as done in tests/e2e/README.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 13:05:48 +03:00
parent 892f39401c
commit 40fc435bda
14 changed files with 1140 additions and 28 deletions
@@ -129,6 +129,14 @@ internal sealed record WebBatchSessionRow(
string? CoverImageUrl = null);
internal sealed record WebTemplateGroupDto(long TelegramChatId);
internal sealed record WebTemplateTopicDestination(int? MessageThreadId, bool TopicCreatedByBot);
internal sealed record WebDeleteSessionInfo(
Guid Id,
string Title,
Guid BatchId,
long TelegramChatId,
int? BatchMessageId,
int? ThreadId,
bool TopicCreatedByBot);
internal sealed record WebPublicGroupRow(
Guid GroupId,
string Name,
@@ -1086,6 +1094,95 @@ public sealed class SessionService(
}
}
public async Task<string?> DeleteSessionAsync(Guid sessionId, Guid groupId)
{
await using var conn = await dataSource.OpenConnectionAsync();
await using var transaction = await conn.BeginTransactionAsync();
var session = await conn.QuerySingleOrDefaultAsync<WebDeleteSessionInfo>(
"""
SELECT s.id AS Id,
s.title AS Title,
s.batch_id AS BatchId,
g.external_group_id::BIGINT AS TelegramChatId,
s.batch_message_id AS BatchMessageId,
s.thread_id AS ThreadId,
s.topic_created_by_bot AS TopicCreatedByBot
FROM sessions s
JOIN game_groups g ON g.id = s.group_id
WHERE s.id = @SessionId AND s.group_id = @GroupId
FOR UPDATE
""",
new { SessionId = sessionId, GroupId = groupId },
transaction);
if (session is null)
{
throw new SessionAccessDeniedException(sessionId, "0");
}
await conn.ExecuteAsync(
"""
UPDATE portfolio_games pg
SET is_public = false,
updated_at = now()
FROM portfolio_game_sessions pgs
WHERE pgs.portfolio_game_id = pg.id
AND pgs.session_id = @SessionId
AND pg.is_public = true
""",
new { SessionId = sessionId },
transaction);
await conn.ExecuteAsync(
"DELETE FROM sessions WHERE id = @Id AND group_id = @GroupId",
new { Id = sessionId, GroupId = groupId },
transaction);
var remainingInTopic = session.ThreadId.HasValue
? await conn.ExecuteScalarAsync<int>(
"""
SELECT COUNT(*)
FROM sessions
WHERE group_id = @GroupId
AND thread_id = @ThreadId
""",
new { GroupId = groupId, ThreadId = session.ThreadId.Value },
transaction)
: 0;
await transaction.CommitAsync();
if (session.ThreadId.HasValue && session.TopicCreatedByBot && remainingInTopic == 0)
{
try
{
await bot.DeleteForumTopic(
chatId: session.TelegramChatId,
messageThreadId: session.ThreadId.Value);
logger.LogInformation(
"Deleted forum topic {ThreadId} for group {GroupId} as no sessions remained.",
session.ThreadId.Value,
groupId);
}
catch (Exception ex)
{
logger.LogWarning(
ex,
"Failed to delete forum topic {ThreadId} for group {GroupId}",
session.ThreadId.Value,
groupId);
}
}
if (session.BatchMessageId.HasValue)
{
await TryUpdateBatchMessageAsync(session.BatchId, session.TelegramChatId, session.BatchMessageId.Value, session.Title);
}
return session.Title;
}
public async Task PromoteWaitlistedPlayerAsync(Guid sessionId, Guid groupId)
{
await using var conn = await dataSource.OpenConnectionAsync();