Files
vtuber-awards/Backend/Endpoints/PublicOverviewEndpoints.cs
T
AzuTear b53c7fb736 Add viewer-range categories, nomination tracking, dynamic showact form, session timeout, share URLs, and workflow-per-season
Features:
- Category viewer ranges + subcategory templates (admin group modal, tree workspace)
- Nomination enrichment via TwitchTracker API (NominationEnrichmentService,
  TwitchTrackerViewerStatsProvider) with admin tracking rules editor
- Nomination group tracker: CategoryGroupName as primary identifier,
  CategoryId stays as nullable legacy field; StreamerIdentity table
- Dynamic showact application form builder (AdminShowactFormBuilder,
  ShowactApplicationSchedule)
- Session idle timeout setting (AdminSessionTimeoutCard)
- Share URLs for X and Discord (SiteSettings, public extras)
- Workflow rules now stored per season (falls back to global SiteSettings)
- New admin routes: settings/access, settings/workflows, tracking-rules
- New admin review workspace with subcategory tabs
- AdminCategoriesView rebuilt with group/subcategory modals

Migrations (all additive):
- AddShareUrls, AddShowactDynamicForm, AddCategoryViewerRanges,
  AddSessionIdleTimeoutSettings, AddSeasonSubcategoryTemplates,
  AddNominationGroupTrackerIdentity, AddShowactApplicationSchedule,
  AddSeasonWorkflowRulesJson

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 23:32:21 +02:00

170 lines
7.3 KiB
C#

using Backend.Common;
using Backend.Contracts;
using Backend.Data;
using Microsoft.EntityFrameworkCore;
namespace Backend.Endpoints;
public static partial class PublicEndpoints
{
private static async Task<IResult> GetOverview(AwardsDbContext db)
{
var siteSettings = await db.SiteSettings.AsNoTracking().FirstOrDefaultAsync(item => item.Id == 1);
var season = await db.Seasons
.AsNoTracking()
.Include(item => item.Categories.OrderBy(category => category.SortOrder))
.ThenInclude(category => category.Candidates)
.OrderByDescending(item => item.Year)
.FirstOrDefaultAsync(item => item.IsCurrent);
if (season is null)
{
return Results.NotFound();
}
if (siteSettings is null)
{
return Results.Problem("Site settings are missing.");
}
var today = DateOnly.FromDateTime(DateTime.UtcNow);
var showactApplicationsOpenNow = ShowactApplicationSchedule.IsOpenNow(siteSettings, today);
var canExposeCurrentSeasonWinners = CanExposeCurrentSeasonWinners(season.CurrentPhase);
var winnerPreviewRows = await db.Results
.AsNoTracking()
.Include(result => result.Season)
.Include(result => result.Candidate)
.Where(result =>
result.Season.Year < season.Year
|| canExposeCurrentSeasonWinners && result.Season.Year == season.Year)
.OrderByDescending(result => result.Season.Year)
.ThenBy(result => result.CategoryName)
.Take(8)
.Select(result => new
{
Year = result.Season.Year,
result.CategoryName,
WinnerName = result.Candidate.DisplayName,
WinnerSlug = result.Candidate.ChannelSlug,
WinnerPlatform = result.Candidate.Platform,
ClipUrl = result.Candidate.ClipCompilationUrl,
ClipTitle = result.Candidate.ClipCompilationTitle,
ClipPlatform = result.Candidate.ClipCompilationPlatform,
ClipEmbedStatus = result.Candidate.ClipEmbedStatus,
})
.ToArrayAsync();
var winnerPreviewItems = winnerPreviewRows
.Select(result => new WinnerPreviewDto(
result.Year,
result.CategoryName,
result.WinnerName,
result.WinnerSlug,
result.WinnerPlatform,
SeasonMappings.BuildProfileUrl(result.WinnerPlatform, result.WinnerSlug),
result.ClipUrl,
result.ClipTitle,
result.ClipPlatform,
result.ClipEmbedStatus))
.ToArray();
var archiveYearRows = await db.Results
.AsNoTracking()
.Where(result =>
result.Season.Year < season.Year
|| canExposeCurrentSeasonWinners && result.Season.Year == season.Year)
.GroupBy(result => result.Season.Year)
.Select(group => new
{
Year = group.Key,
WinnerCount = group.Count(),
})
.OrderByDescending(item => item.Year)
.ToArrayAsync();
var archiveYears = archiveYearRows
.Select(item => new ArchiveYearDto(item.Year, item.WinnerCount))
.ToArray();
var phaseKey = SeasonMappings.NormalizePhaseKey(season.CurrentPhase);
var publicCategories = season.Categories
.Where(category => ShouldExposePublicCategory(phaseKey, category.Candidates.Count))
.ToArray();
var featuredCategories = publicCategories
.GroupBy(category => category.GroupName.Trim(), StringComparer.OrdinalIgnoreCase)
.Select(group =>
{
var ordered = group
.OrderBy(category => category.SortOrder)
.ThenBy(category => category.Name, StringComparer.OrdinalIgnoreCase)
.ToArray();
var first = ordered[0];
var maxNomineesPerUser = ordered
.Select(category => category.MaxNomineesPerUser)
.Where(value => value > 0)
.DefaultIfEmpty(3)
.Max();
return new FeaturedCategoryDto(
first.Id,
first.GroupName,
first.GroupName,
first.Description,
maxNomineesPerUser);
})
.OrderBy(category => publicCategories
.Where(item => string.Equals(item.GroupName, category.GroupName, StringComparison.OrdinalIgnoreCase))
.Min(item => item.SortOrder))
.ThenBy(category => category.GroupName, StringComparer.OrdinalIgnoreCase)
.ToArray();
var response = new OverviewResponse(
season.Id,
season.Year,
season.Name,
season.ShowDate,
season.ShowStartsAt,
SeasonMappings.NormalizeSeasonStreamUrl(season.ShowStreamUrl),
season.CurrentPhase,
season.IsCommunityOnly,
"Twitch",
new[]
{
new TimelineItem("nomination", "Nominierung", season.NominationStartsAt, season.NominationEndsAt, SeasonMappings.ResolveTimelineState("nomination", phaseKey)),
new TimelineItem("voting", "Voting", season.VotingStartsAt, season.VotingEndsAt, SeasonMappings.ResolveTimelineState("voting", phaseKey)),
new TimelineItem("preparation", "Aufbereitung", season.ReviewStartsAt, season.ReviewEndsAt, SeasonMappings.ResolveTimelineState("preparation", phaseKey)),
new TimelineItem("show", "Award Show", season.ShowDate, season.ShowDate, SeasonMappings.ResolveTimelineState("show", phaseKey)),
},
featuredCategories,
winnerPreviewItems,
archiveYears,
new PublicSiteContentDto(
siteSettings.HostDisplayName,
siteSettings.HostTagline,
siteSettings.NewsletterUrl,
siteSettings.ShareXUrl,
siteSettings.ShareDiscordUrl,
siteSettings.PrivacyEmail,
siteSettings.PrivacyPolicyContent,
SeasonMappings.ReadSocialLinks(siteSettings),
SeasonMappings.BuildFooterLinks(siteSettings)),
new PublicFeatureFlagsDto(
siteSettings.ClipSubmissionsEnabled,
siteSettings.ClipReviewEnabled,
string.IsNullOrWhiteSpace(siteSettings.ClipSubmissionDisabledMessage)
? "Clip-Einreichungen sind aktuell geschlossen."
: siteSettings.ClipSubmissionDisabledMessage,
showactApplicationsOpenNow,
siteSettings.ShowactApplicationStartsAt,
siteSettings.ShowactApplicationEndsAt,
string.IsNullOrWhiteSpace(siteSettings.ShowactApplicationDisabledMessage)
? "Showact-Bewerbungen sind aktuell geschlossen."
: siteSettings.ShowactApplicationDisabledMessage,
siteSettings.SponsorsVisible,
siteSettings.ShowactFormSchemaJson ?? "[]"),
SeasonMappings.ReadFaqItems(siteSettings));
return Results.Ok(response);
}
}