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
+23
View File
@@ -19,6 +19,7 @@ public static class WorkflowRuleSettings
public const string MaxCandidateAppearances = "max_candidate_appearances";
public const string MaxWinnerPlacements = "max_winner_placements";
public const string WinnerRequiresClip = "winner_requires_clip";
public const string RecommendedNominatorsPerSubcategory = "recommended_nominators_per_subcategory";
public static WorkflowRuleSetting[] Defaults { get; } =
[
@@ -26,11 +27,28 @@ public static class WorkflowRuleSettings
new(MaxCandidateAppearances, "Kandidaturen pro Person", true, 2, "warn", "Warnt oder blockiert, wenn dieselbe Person in zu vielen Kategorien final auftaucht."),
new(MaxWinnerPlacements, "Gewinnerplätze pro Person", true, 1, "block", "Verhindert oder warnt, wenn dieselbe Person mehrfach als Gewinner:in gesetzt wird."),
new(WinnerRequiresClip, "Gewinner braucht Clip-Link", true, 1, "block", "Verhindert oder warnt, wenn ein Gewinner ohne gepflegten YouTube-/Twitch-Clip gesetzt wird."),
new(RecommendedNominatorsPerSubcategory, "Empfohlene Nominierer pro Unterkategorie", true, 4, "warn", "Zeigt in der Kategorie-Übersicht an, ab wann eine Unterkategorie nominierungsseitig gut getragen ist. Diese Regel blockiert nichts."),
];
public static WorkflowRuleSetting[] Read(SiteSettings? settings)
{
var storedRules = Parse(settings?.WorkflowRulesJson);
return Read(storedRules);
}
public static WorkflowRuleSetting[] Read(Season? season, SiteSettings? settings)
{
var seasonRules = Parse(season?.WorkflowRulesJson);
if (seasonRules.Length > 0)
{
return Read(seasonRules);
}
return Read(settings);
}
private static WorkflowRuleSetting[] Read(WorkflowRuleSetting[] storedRules)
{
return Defaults
.Select(defaultRule =>
{
@@ -98,6 +116,11 @@ public static class WorkflowRuleSettings
_ => fallback.Mode,
};
if (string.Equals(fallback.Key, RecommendedNominatorsPerSubcategory, StringComparison.OrdinalIgnoreCase))
{
mode = "warn";
}
return rule with
{
Key = fallback.Key,