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>
This commit is contained in:
AzuTear
2026-06-28 23:32:21 +02:00
parent 4b2c5fa15d
commit b53c7fb736
178 changed files with 28933 additions and 1908 deletions
+45 -12
View File
@@ -26,12 +26,18 @@ public static partial class PublicEndpoints
{
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)
.Where(result =>
result.Season.Year < season.Year
|| canExposeCurrentSeasonWinners && result.Season.Year == season.Year)
.OrderByDescending(result => result.Season.Year)
.ThenBy(result => result.CategoryName)
.Take(8)
@@ -65,7 +71,9 @@ public static partial class PublicEndpoints
var archiveYearRows = await db.Results
.AsNoTracking()
.Where(result => result.Season.Year < season.Year)
.Where(result =>
result.Season.Year < season.Year
|| canExposeCurrentSeasonWinners && result.Season.Year == season.Year)
.GroupBy(result => result.Season.Year)
.Select(group => new
{
@@ -83,6 +91,33 @@ public static partial class PublicEndpoints
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,
@@ -100,20 +135,15 @@ public static partial class PublicEndpoints
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)),
},
publicCategories
.Select(category => new FeaturedCategoryDto(
category.Id,
category.GroupName,
category.Name,
category.Description,
category.MaxNomineesPerUser))
.ToArray(),
featuredCategories,
winnerPreviewItems,
archiveYears,
new PublicSiteContentDto(
siteSettings.HostDisplayName,
siteSettings.HostTagline,
siteSettings.NewsletterUrl,
siteSettings.ShareXUrl,
siteSettings.ShareDiscordUrl,
siteSettings.PrivacyEmail,
siteSettings.PrivacyPolicyContent,
SeasonMappings.ReadSocialLinks(siteSettings),
@@ -124,11 +154,14 @@ public static partial class PublicEndpoints
string.IsNullOrWhiteSpace(siteSettings.ClipSubmissionDisabledMessage)
? "Clip-Einreichungen sind aktuell geschlossen."
: siteSettings.ClipSubmissionDisabledMessage,
siteSettings.ShowactApplicationsEnabled,
showactApplicationsOpenNow,
siteSettings.ShowactApplicationStartsAt,
siteSettings.ShowactApplicationEndsAt,
string.IsNullOrWhiteSpace(siteSettings.ShowactApplicationDisabledMessage)
? "Showact-Bewerbungen sind aktuell geschlossen."
: siteSettings.ShowactApplicationDisabledMessage,
siteSettings.SponsorsVisible),
siteSettings.SponsorsVisible,
siteSettings.ShowactFormSchemaJson ?? "[]"),
SeasonMappings.ReadFaqItems(siteSettings));
return Results.Ok(response);