b53c7fb736
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>
135 lines
5.1 KiB
C#
135 lines
5.1 KiB
C#
using System.Text.Json;
|
|
using Backend.Domain;
|
|
|
|
namespace Backend.Services;
|
|
|
|
public sealed record WorkflowRuleSetting(
|
|
string Key,
|
|
string Label,
|
|
bool Enabled,
|
|
int Limit,
|
|
string Mode,
|
|
string Description);
|
|
|
|
public static class WorkflowRuleSettings
|
|
{
|
|
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
|
|
|
public const string MaxFinalistsPerCategory = "max_finalists_per_category";
|
|
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; } =
|
|
[
|
|
new(MaxFinalistsPerCategory, "Finale Kandidat:innen pro Kategorie", true, 4, "block", "Begrenzt, wie viele finale Kandidat:innen in einer Kategorie vorbereitet werden."),
|
|
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 =>
|
|
{
|
|
var storedRule = storedRules.FirstOrDefault(item => string.Equals(item.Key, defaultRule.Key, StringComparison.OrdinalIgnoreCase));
|
|
return storedRule is null ? defaultRule : Normalize(storedRule, defaultRule);
|
|
})
|
|
.ToArray();
|
|
}
|
|
|
|
public static string Serialize(IEnumerable<WorkflowRuleSetting> rules) =>
|
|
JsonSerializer.Serialize(rules.Select(rule => Normalize(rule, Defaults.FirstOrDefault(item => item.Key == rule.Key) ?? rule)), JsonOptions);
|
|
|
|
public static WorkflowRuleSetting Find(IEnumerable<WorkflowRuleSetting> rules, string key) =>
|
|
rules.FirstOrDefault(item => string.Equals(item.Key, key, StringComparison.OrdinalIgnoreCase))
|
|
?? Defaults.First(item => item.Key == key);
|
|
|
|
public static bool ShouldBlock(WorkflowRuleSetting rule) =>
|
|
rule.Enabled && string.Equals(rule.Mode, "block", StringComparison.OrdinalIgnoreCase);
|
|
|
|
public static string CandidateIdentityKey(Candidate candidate)
|
|
{
|
|
var channel = candidate.ChannelSlug.Trim().TrimStart('@').ToLowerInvariant();
|
|
if (!string.IsNullOrWhiteSpace(channel))
|
|
{
|
|
return $"slug:{channel}";
|
|
}
|
|
|
|
return $"name:{candidate.DisplayName.Trim().ToLowerInvariant()}";
|
|
}
|
|
|
|
public static string CandidateIdentityKey(string displayName, string channelSlug)
|
|
{
|
|
var channel = channelSlug.Trim().TrimStart('@').ToLowerInvariant();
|
|
if (!string.IsNullOrWhiteSpace(channel))
|
|
{
|
|
return $"slug:{channel}";
|
|
}
|
|
|
|
return $"name:{displayName.Trim().ToLowerInvariant()}";
|
|
}
|
|
|
|
private static WorkflowRuleSetting[] Parse(string? json)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(json))
|
|
{
|
|
return [];
|
|
}
|
|
|
|
try
|
|
{
|
|
return JsonSerializer.Deserialize<WorkflowRuleSetting[]>(json, JsonOptions) ?? [];
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
return [];
|
|
}
|
|
}
|
|
|
|
private static WorkflowRuleSetting Normalize(WorkflowRuleSetting rule, WorkflowRuleSetting fallback)
|
|
{
|
|
var mode = rule.Mode.Trim().ToLowerInvariant() switch
|
|
{
|
|
"block" => "block",
|
|
"warn" => "warn",
|
|
_ => fallback.Mode,
|
|
};
|
|
|
|
if (string.Equals(fallback.Key, RecommendedNominatorsPerSubcategory, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
mode = "warn";
|
|
}
|
|
|
|
return rule with
|
|
{
|
|
Key = fallback.Key,
|
|
Label = string.IsNullOrWhiteSpace(rule.Label) ? fallback.Label : rule.Label.Trim(),
|
|
Enabled = rule.Enabled,
|
|
Limit = Math.Clamp(rule.Limit, 1, 50),
|
|
Mode = mode,
|
|
Description = string.IsNullOrWhiteSpace(rule.Description) ? fallback.Description : rule.Description.Trim(),
|
|
};
|
|
}
|
|
}
|