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:
@@ -41,6 +41,22 @@ public static class AdminSiteSettingsEndpoints
|
||||
.AddEndpointFilter((context, next) => AdminEndpointConventions.RequireWritePermission(context, next, AdminPermissionCatalog.Settings))
|
||||
.WithName("UpdateAdminOptionalFeatureSettings")
|
||||
.WithOpenApi();
|
||||
group.MapGet("/tracking-rules", GetTrackingRules)
|
||||
.AddEndpointFilter((context, next) => AdminEndpointConventions.RequirePermission(context, next, AdminPermissionCatalog.Settings))
|
||||
.WithName("GetAdminTrackingRules")
|
||||
.WithOpenApi();
|
||||
group.MapPut("/tracking-rules", UpdateTrackingRules)
|
||||
.AddEndpointFilter((context, next) => AdminEndpointConventions.RequireWritePermission(context, next, AdminPermissionCatalog.Settings))
|
||||
.WithName("UpdateAdminTrackingRules")
|
||||
.WithOpenApi();
|
||||
group.MapPut("/tracking-rules/source", UpdateTrackingSource)
|
||||
.AddEndpointFilter((context, next) => AdminEndpointConventions.RequireWritePermission(context, next, AdminPermissionCatalog.Settings))
|
||||
.WithName("UpdateAdminTrackingSource")
|
||||
.WithOpenApi();
|
||||
group.MapPut("/tracking-rules/notes", UpdateTrackingReviewNotes)
|
||||
.AddEndpointFilter((context, next) => AdminEndpointConventions.RequireWritePermission(context, next, AdminPermissionCatalog.Settings))
|
||||
.WithName("UpdateAdminTrackingReviewNotes")
|
||||
.WithOpenApi();
|
||||
return group;
|
||||
}
|
||||
|
||||
@@ -56,6 +72,8 @@ public static class AdminSiteSettingsEndpoints
|
||||
settings.HostDisplayName,
|
||||
settings.HostTagline,
|
||||
settings.NewsletterUrl,
|
||||
settings.ShareXUrl,
|
||||
settings.ShareDiscordUrl,
|
||||
settings.PrivacyEmail,
|
||||
settings.PrivacyPolicyContent,
|
||||
settings.PrivacyPolicyUpdatedBy,
|
||||
@@ -69,7 +87,8 @@ public static class AdminSiteSettingsEndpoints
|
||||
settings.ShowactsUrl,
|
||||
settings.ShowactsContent,
|
||||
SeasonMappings.ReadSocialLinks(settings),
|
||||
SeasonMappings.ReadFaqItems(settings)));
|
||||
SeasonMappings.ReadFaqItems(settings),
|
||||
settings.ShowactFormSchemaJson ?? "[]"));
|
||||
}
|
||||
|
||||
private static async Task<IResult> UpdateSiteSettings(
|
||||
@@ -95,6 +114,8 @@ public static class AdminSiteSettingsEndpoints
|
||||
settings.HostDisplayName = request.HostDisplayName.Trim();
|
||||
settings.HostTagline = request.HostTagline.Trim();
|
||||
settings.NewsletterUrl = normalizedUrls.NewsletterUrl;
|
||||
settings.ShareXUrl = normalizedUrls.ShareXUrl;
|
||||
settings.ShareDiscordUrl = normalizedUrls.ShareDiscordUrl;
|
||||
settings.PrivacyEmail = request.PrivacyEmail.Trim();
|
||||
var trimmedPrivacyContent = request.PrivacyPolicyContent.Trim();
|
||||
var privacyChanged = !string.Equals(settings.PrivacyPolicyContent, trimmedPrivacyContent, StringComparison.Ordinal);
|
||||
@@ -115,6 +136,7 @@ public static class AdminSiteSettingsEndpoints
|
||||
settings.ShowactsContent = request.ShowactsContent.Trim();
|
||||
settings.SocialLinksJson = JsonSerializer.Serialize(socialLinks);
|
||||
settings.FaqJson = JsonSerializer.Serialize(request.Faq ?? []);
|
||||
settings.ShowactFormSchemaJson = request.ShowactFormSchemaJson ?? "[]";
|
||||
|
||||
adminAuditService.AddEntry(
|
||||
session.TwitchUserId,
|
||||
@@ -144,6 +166,8 @@ public static class AdminSiteSettingsEndpoints
|
||||
socialLinks = [];
|
||||
|
||||
if (!TryNormalizePublicUrl(request.NewsletterUrl, "Newsletter-Link", out var newsletterUrl, out var errorMessage)
|
||||
|| !TryNormalizePublicUrl(request.ShareXUrl, "X-Teilen-Link", out var shareXUrl, out errorMessage)
|
||||
|| !TryNormalizePublicUrl(request.ShareDiscordUrl, "Discord-Teilen-Link", out var shareDiscordUrl, out errorMessage)
|
||||
|| !TryNormalizePublicUrl(request.ImprintUrl, "Impressum-Link", out var imprintUrl, out errorMessage)
|
||||
|| !TryNormalizePublicUrl(request.ContactUrl, "Kontakt-Link", out var contactUrl, out errorMessage)
|
||||
|| !TryNormalizePublicUrl(request.SponsorsUrl, "Sponsoren-Link", out var sponsorsUrl, out errorMessage)
|
||||
@@ -155,6 +179,8 @@ public static class AdminSiteSettingsEndpoints
|
||||
normalizedUrls = new PublicSiteUrlSettings
|
||||
{
|
||||
NewsletterUrl = newsletterUrl,
|
||||
ShareXUrl = shareXUrl,
|
||||
ShareDiscordUrl = shareDiscordUrl,
|
||||
ImprintUrl = imprintUrl,
|
||||
ContactUrl = contactUrl,
|
||||
SponsorsUrl = sponsorsUrl,
|
||||
@@ -199,6 +225,8 @@ public static class AdminSiteSettingsEndpoints
|
||||
private sealed class PublicSiteUrlSettings
|
||||
{
|
||||
public string NewsletterUrl { get; set; } = string.Empty;
|
||||
public string ShareXUrl { get; set; } = string.Empty;
|
||||
public string ShareDiscordUrl { get; set; } = string.Empty;
|
||||
public string ImprintUrl { get; set; } = string.Empty;
|
||||
public string ContactUrl { get; set; } = string.Empty;
|
||||
public string SponsorsUrl { get; set; } = string.Empty;
|
||||
@@ -216,6 +244,160 @@ public static class AdminSiteSettingsEndpoints
|
||||
return Results.Ok(ToOptionalFeatureSettingsResponse(settings));
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetTrackingRules(AwardsDbContext db)
|
||||
{
|
||||
var settings = await db.SiteSettings.AsNoTracking().FirstOrDefaultAsync(item => item.Id == 1);
|
||||
if (settings is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
return Results.Ok(ToTrackingRulesResponse(settings));
|
||||
}
|
||||
|
||||
private static async Task<IResult> UpdateTrackingRules(
|
||||
HttpContext context,
|
||||
UpdateTrackingRulesRequest request,
|
||||
AwardsDbContext db,
|
||||
IAdminAuditService adminAuditService,
|
||||
NominationTrackingReviewService trackingReviewService)
|
||||
{
|
||||
var session = AdminEndpointConventions.CurrentSession(context);
|
||||
var settings = await db.SiteSettings.FirstOrDefaultAsync(item => item.Id == 1);
|
||||
if (settings is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
var before = ToTrackingRulesResponse(settings);
|
||||
var applyResult = ApplyTrackingRulesRequest(settings, request);
|
||||
if (applyResult is not null)
|
||||
{
|
||||
return applyResult;
|
||||
}
|
||||
|
||||
await trackingReviewService.ReevaluateAllAsync(TrackingRulesSettings.Read(settings), true, context.RequestAborted);
|
||||
|
||||
var after = ToTrackingRulesResponse(settings);
|
||||
adminAuditService.AddEntry(
|
||||
session.TwitchUserId,
|
||||
"tracking-rules.update",
|
||||
"site-settings",
|
||||
settings.Id.ToString(),
|
||||
"Tracking Rules wurden aktualisiert.",
|
||||
new
|
||||
{
|
||||
sourceChanged = before.Source.BaseUrl != after.Source.BaseUrl,
|
||||
manualReviewNotesChanged = before.ManualReviewNotes != after.ManualReviewNotes,
|
||||
importantMetricCount = after.ImportantMetrics.Count(item => item.Enabled),
|
||||
optionalMetricCount = after.OptionalMetrics.Count(item => item.Enabled),
|
||||
flagCount = after.Flags.Count(item => item.Enabled),
|
||||
},
|
||||
RequestMetadataReader.Read(context));
|
||||
|
||||
await db.SaveChangesAsync(context.RequestAborted);
|
||||
return Results.Ok(after);
|
||||
}
|
||||
|
||||
private static async Task<IResult> UpdateTrackingSource(
|
||||
HttpContext context,
|
||||
UpdateTrackingSourceRequest request,
|
||||
AwardsDbContext db,
|
||||
IAdminAuditService adminAuditService,
|
||||
NominationTrackingReviewService trackingReviewService)
|
||||
{
|
||||
var session = AdminEndpointConventions.CurrentSession(context);
|
||||
var settings = await db.SiteSettings.FirstOrDefaultAsync(item => item.Id == 1);
|
||||
if (settings is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
if (!TryNormalizeTrackingSourceUrl(request.Source?.BaseUrl, out var normalizedBaseUrl, out var errorMessage))
|
||||
{
|
||||
return Results.BadRequest(new { message = errorMessage });
|
||||
}
|
||||
|
||||
var before = ToTrackingRulesResponse(settings);
|
||||
var rules = TrackingRulesSettings.Read(settings);
|
||||
var updatedRules = rules with
|
||||
{
|
||||
Source = new TrackingSourceSetting(
|
||||
TrackingRulesSettings.ProviderKey,
|
||||
normalizedBaseUrl,
|
||||
request.Source?.NotesSummary ?? rules.Source.NotesSummary,
|
||||
request.Source?.ShowManualReviewNotesInReview ?? rules.Source.ShowManualReviewNotesInReview),
|
||||
};
|
||||
|
||||
settings.ViewerStatsProviderBaseUrl = normalizedBaseUrl;
|
||||
settings.TrackingRulesJson = TrackingRulesSettings.Serialize(updatedRules);
|
||||
|
||||
await trackingReviewService.ReevaluateAllAsync(TrackingRulesSettings.Read(settings), true, context.RequestAborted);
|
||||
|
||||
var after = ToTrackingRulesResponse(settings);
|
||||
adminAuditService.AddEntry(
|
||||
session.TwitchUserId,
|
||||
"tracking-source.update",
|
||||
"site-settings",
|
||||
settings.Id.ToString(),
|
||||
"Tracking Source wurde aktualisiert.",
|
||||
new
|
||||
{
|
||||
beforeBaseUrl = before.Source.BaseUrl,
|
||||
afterBaseUrl = after.Source.BaseUrl,
|
||||
},
|
||||
RequestMetadataReader.Read(context));
|
||||
|
||||
await db.SaveChangesAsync(context.RequestAborted);
|
||||
return Results.Ok(after);
|
||||
}
|
||||
|
||||
private static async Task<IResult> UpdateTrackingReviewNotes(
|
||||
HttpContext context,
|
||||
UpdateTrackingReviewNotesRequest request,
|
||||
AwardsDbContext db,
|
||||
IAdminAuditService adminAuditService)
|
||||
{
|
||||
var session = AdminEndpointConventions.CurrentSession(context);
|
||||
var settings = await db.SiteSettings.FirstOrDefaultAsync(item => item.Id == 1);
|
||||
if (settings is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
var beforeNotes = settings.TrackingReviewNotes ?? string.Empty;
|
||||
var before = ToTrackingRulesResponse(settings);
|
||||
var rules = TrackingRulesSettings.Read(settings);
|
||||
settings.TrackingReviewNotes = (request.ManualReviewNotes ?? string.Empty).Trim();
|
||||
settings.TrackingRulesJson = TrackingRulesSettings.Serialize(rules with
|
||||
{
|
||||
Source = rules.Source with
|
||||
{
|
||||
ShowManualReviewNotesInReview = request.ShowManualReviewNotesInReview,
|
||||
},
|
||||
});
|
||||
|
||||
var after = ToTrackingRulesResponse(settings);
|
||||
|
||||
adminAuditService.AddEntry(
|
||||
session.TwitchUserId,
|
||||
"tracking-review-notes.update",
|
||||
"site-settings",
|
||||
settings.Id.ToString(),
|
||||
"Tracking Review Notes wurden aktualisiert.",
|
||||
new
|
||||
{
|
||||
beforeLength = beforeNotes.Length,
|
||||
afterLength = settings.TrackingReviewNotes.Length,
|
||||
beforeShowInReview = before.Source.ShowManualReviewNotesInReview,
|
||||
afterShowInReview = after.Source.ShowManualReviewNotesInReview,
|
||||
},
|
||||
RequestMetadataReader.Read(context));
|
||||
|
||||
await db.SaveChangesAsync(context.RequestAborted);
|
||||
return Results.Ok(after);
|
||||
}
|
||||
|
||||
private static async Task<IResult> UpdateOptionalFeatureSettings(
|
||||
HttpContext context,
|
||||
UpdateOptionalFeatureSettingsRequest request,
|
||||
@@ -230,6 +412,12 @@ public static class AdminSiteSettingsEndpoints
|
||||
}
|
||||
|
||||
var before = ToOptionalFeatureSettingsResponse(settings);
|
||||
var scheduleValidationError = ShowactApplicationSchedule.Validate(request.ShowactApplicationStartsAt, request.ShowactApplicationEndsAt);
|
||||
if (scheduleValidationError is not null)
|
||||
{
|
||||
return Results.BadRequest(new { message = scheduleValidationError });
|
||||
}
|
||||
|
||||
var disabledMessage = NormalizeOptionalFeatureText(
|
||||
request.ClipSubmissionDisabledMessage,
|
||||
FallbackClipSubmissionDisabledMessage,
|
||||
@@ -244,6 +432,8 @@ public static class AdminSiteSettingsEndpoints
|
||||
settings.ClipAdminMenuVisible = request.ClipAdminMenuVisible;
|
||||
settings.ClipSubmissionDisabledMessage = disabledMessage;
|
||||
settings.ShowactApplicationsEnabled = request.ShowactApplicationsEnabled;
|
||||
settings.ShowactApplicationStartsAt = request.ShowactApplicationStartsAt;
|
||||
settings.ShowactApplicationEndsAt = request.ShowactApplicationEndsAt;
|
||||
settings.ShowactApplicationDisabledMessage = showactDisabledMessage;
|
||||
settings.SponsorsVisible = request.SponsorsVisible;
|
||||
|
||||
@@ -275,6 +465,9 @@ public static class AdminSiteSettingsEndpoints
|
||||
? FallbackClipSubmissionDisabledMessage
|
||||
: settings.ClipSubmissionDisabledMessage,
|
||||
settings.ShowactApplicationsEnabled,
|
||||
settings.ShowactApplicationStartsAt,
|
||||
settings.ShowactApplicationEndsAt,
|
||||
ShowactApplicationSchedule.IsOpenNow(settings, DateOnly.FromDateTime(DateTime.UtcNow)),
|
||||
string.IsNullOrWhiteSpace(settings.ShowactApplicationDisabledMessage)
|
||||
? "Showact-Bewerbungen sind aktuell geschlossen."
|
||||
: settings.ShowactApplicationDisabledMessage,
|
||||
@@ -301,6 +494,8 @@ public static class AdminSiteSettingsEndpoints
|
||||
AddOperationalChange(changes, "clipAdminMenuVisible", "Clips-Menüpunkt", before.ClipAdminMenuVisible, after.ClipAdminMenuVisible);
|
||||
AddOperationalChange(changes, "clipSubmissionDisabledMessage", "Deaktiviert-Hinweis", before.ClipSubmissionDisabledMessage, after.ClipSubmissionDisabledMessage);
|
||||
AddOperationalChange(changes, "showactApplicationsEnabled", "Showact-Bewerbungen", before.ShowactApplicationsEnabled, after.ShowactApplicationsEnabled);
|
||||
AddOperationalChange(changes, "showactApplicationStartsAt", "Showact Start", before.ShowactApplicationStartsAt, after.ShowactApplicationStartsAt);
|
||||
AddOperationalChange(changes, "showactApplicationEndsAt", "Showact Deadline", before.ShowactApplicationEndsAt, after.ShowactApplicationEndsAt);
|
||||
AddOperationalChange(changes, "showactApplicationDisabledMessage", "Showact-Hinweis", before.ShowactApplicationDisabledMessage, after.ShowactApplicationDisabledMessage);
|
||||
AddOperationalChange(changes, "sponsorsVisible", "Sponsoren sichtbar", before.SponsorsVisible, after.SponsorsVisible);
|
||||
return changes.ToArray();
|
||||
@@ -314,7 +509,7 @@ public static class AdminSiteSettingsEndpoints
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
var usesDatabaseDemo = settings.DemoLoginManagedByDatabase || HasDatabaseDemoCredentials(settings);
|
||||
var usesDatabaseDemo = settings.DemoLoginManagedByDatabase;
|
||||
var twitchSettings = ReadEffectiveTwitchSettings(settings, configuration);
|
||||
return Results.Ok(new AdminOperationalSettingsResponse(
|
||||
usesDatabaseDemo,
|
||||
@@ -329,6 +524,7 @@ public static class AdminSiteSettingsEndpoints
|
||||
twitchSettings.ClientSecretSet,
|
||||
twitchSettings.RedirectUri,
|
||||
twitchSettings.Scope,
|
||||
UserSessionService.NormalizeIdleTimeoutHours(settings.SessionIdleTimeoutHours),
|
||||
settings.MaintenanceModeEnabled,
|
||||
string.IsNullOrWhiteSpace(settings.MaintenanceTitle) ? FallbackMaintenanceTitle : settings.MaintenanceTitle,
|
||||
string.IsNullOrWhiteSpace(settings.MaintenanceMessage)
|
||||
@@ -365,6 +561,12 @@ public static class AdminSiteSettingsEndpoints
|
||||
var twitchClientSecret = request.TwitchClientSecret?.Trim() ?? string.Empty;
|
||||
var twitchRedirectUri = request.TwitchRedirectUri.Trim();
|
||||
var twitchScope = request.TwitchScope.Trim();
|
||||
if (request.SessionIdleTimeoutHours < UserSessionService.MinimumIdleTimeoutHours)
|
||||
{
|
||||
return Results.BadRequest(new { message = $"Session-Timeout muss mindestens {UserSessionService.MinimumIdleTimeoutHours} Stunden betragen." });
|
||||
}
|
||||
|
||||
var sessionIdleTimeoutHours = UserSessionService.NormalizeIdleTimeoutHours(request.SessionIdleTimeoutHours);
|
||||
var existingTwitchSecretAvailable = !string.IsNullOrWhiteSpace(settings.TwitchClientSecret)
|
||||
|| !string.IsNullOrWhiteSpace(ReadTwitchSetting(configuration, "ClientSecret", "VTSA_TWITCH_CLIENT_SECRET"));
|
||||
|
||||
@@ -424,6 +626,7 @@ public static class AdminSiteSettingsEndpoints
|
||||
settings.TwitchClientSecret,
|
||||
settings.TwitchRedirectUri,
|
||||
settings.TwitchScope);
|
||||
settings.SessionIdleTimeoutHours = sessionIdleTimeoutHours;
|
||||
|
||||
var passwordToPersist = !string.IsNullOrWhiteSpace(newPassword)
|
||||
? newPassword
|
||||
@@ -456,11 +659,12 @@ public static class AdminSiteSettingsEndpoints
|
||||
"operational-settings.update",
|
||||
"site-settings",
|
||||
settings.Id.ToString(),
|
||||
"Demo-Zugang und Wartungsmodus wurden aktualisiert.",
|
||||
"Demo-Zugang, Session-Timeout und Wartungsmodus wurden aktualisiert.",
|
||||
new
|
||||
{
|
||||
settings.DemoLoginEnabled,
|
||||
passwordChanged = !string.IsNullOrWhiteSpace(passwordToPersist),
|
||||
settings.SessionIdleTimeoutHours,
|
||||
settings.MaintenanceModeEnabled,
|
||||
changes,
|
||||
},
|
||||
@@ -555,6 +759,7 @@ public static class AdminSiteSettingsEndpoints
|
||||
HasEffectiveTwitchClientSecret(settings, configuration),
|
||||
settings.TwitchRedirectUri,
|
||||
settings.TwitchScope,
|
||||
UserSessionService.NormalizeIdleTimeoutHours(settings.SessionIdleTimeoutHours),
|
||||
settings.MaintenanceModeEnabled,
|
||||
string.IsNullOrWhiteSpace(settings.MaintenanceTitle) ? FallbackMaintenanceTitle : settings.MaintenanceTitle,
|
||||
string.IsNullOrWhiteSpace(settings.MaintenanceMessage) ? FallbackMaintenanceMessage : settings.MaintenanceMessage);
|
||||
@@ -576,6 +781,7 @@ public static class AdminSiteSettingsEndpoints
|
||||
AddOperationalChange(changes, "twitchClientId", "Twitch Client-ID", before.TwitchClientId, after.TwitchClientId);
|
||||
AddOperationalChange(changes, "twitchRedirectUri", "Twitch Redirect URI", before.TwitchRedirectUri, after.TwitchRedirectUri);
|
||||
AddOperationalChange(changes, "twitchScope", "Twitch Scope", before.TwitchScope, after.TwitchScope);
|
||||
AddOperationalChange(changes, "sessionIdleTimeoutHours", "Session Inaktivitaet", before.SessionIdleTimeoutHours, after.SessionIdleTimeoutHours);
|
||||
|
||||
if (before.TwitchClientSecretSet != after.TwitchClientSecretSet || twitchClientSecretChanged)
|
||||
{
|
||||
@@ -653,6 +859,7 @@ public static class AdminSiteSettingsEndpoints
|
||||
bool TwitchClientSecretSet,
|
||||
string TwitchRedirectUri,
|
||||
string TwitchScope,
|
||||
int SessionIdleTimeoutHours,
|
||||
bool MaintenanceModeEnabled,
|
||||
string MaintenanceTitle,
|
||||
string MaintenanceMessage);
|
||||
@@ -663,4 +870,136 @@ public static class AdminSiteSettingsEndpoints
|
||||
string RedirectUri,
|
||||
string Scope,
|
||||
bool Configured);
|
||||
|
||||
private static IResult? ApplyTrackingRulesRequest(SiteSettings settings, UpdateTrackingRulesRequest request)
|
||||
{
|
||||
if (!TryNormalizeTrackingSourceUrl(request.Source?.BaseUrl, out var normalizedBaseUrl, out var errorMessage))
|
||||
{
|
||||
return Results.BadRequest(new { message = errorMessage });
|
||||
}
|
||||
|
||||
var currentRules = TrackingRulesSettings.Read(settings);
|
||||
var configuration = new TrackingRulesConfiguration(
|
||||
new TrackingSourceSetting(
|
||||
TrackingRulesSettings.ProviderKey,
|
||||
normalizedBaseUrl,
|
||||
request.Source?.NotesSummary ?? currentRules.Source.NotesSummary,
|
||||
request.Source?.ShowManualReviewNotesInReview ?? currentRules.Source.ShowManualReviewNotesInReview),
|
||||
(request.ImportantMetrics ?? []).Select(ToTrackingMetricRuleSetting).ToArray(),
|
||||
(request.OptionalMetrics ?? []).Select(ToTrackingMetricRuleSetting).ToArray(),
|
||||
(request.Flags ?? []).Select(ToTrackingFlagRuleSetting).ToArray());
|
||||
|
||||
settings.TrackingRulesJson = TrackingRulesSettings.Serialize(configuration);
|
||||
settings.ViewerStatsProviderBaseUrl = normalizedBaseUrl;
|
||||
settings.TrackingReviewNotes = (request.ManualReviewNotes ?? string.Empty).Trim();
|
||||
return null;
|
||||
}
|
||||
|
||||
private static AdminTrackingRulesResponse ToTrackingRulesResponse(SiteSettings settings)
|
||||
{
|
||||
var rules = TrackingRulesSettings.Read(settings);
|
||||
return new AdminTrackingRulesResponse(
|
||||
new AdminTrackingSourceDto(
|
||||
rules.Source.ProviderKey,
|
||||
"TwitchTracker Basic API",
|
||||
TrackingRulesSettings.NormalizeBaseUrl(settings.ViewerStatsProviderBaseUrl),
|
||||
rules.Source.NotesSummary,
|
||||
rules.Source.ShowManualReviewNotesInReview),
|
||||
rules.ImportantMetrics.Select(ToTrackingMetricRuleDto).ToArray(),
|
||||
rules.OptionalMetrics.Select(ToTrackingMetricRuleDto).ToArray(),
|
||||
rules.Flags.Select(ToTrackingFlagRuleDto).ToArray(),
|
||||
settings.TrackingReviewNotes ?? string.Empty);
|
||||
}
|
||||
|
||||
private static AdminTrackingMetricRuleDto ToTrackingMetricRuleDto(TrackingMetricRuleSetting rule) =>
|
||||
new(
|
||||
rule.Key,
|
||||
rule.Label,
|
||||
rule.Enabled,
|
||||
rule.SourceSupport,
|
||||
rule.Description,
|
||||
rule.RequiredForAutoClassification,
|
||||
rule.ShowInReview,
|
||||
rule.ShowInAdminSummary,
|
||||
rule.ManualOverrideAllowed,
|
||||
rule.WindowKey,
|
||||
rule.AutoSupportedWindowKeys,
|
||||
rule.ProviderFieldKey,
|
||||
rule.TopCount,
|
||||
rule.MinPrimaryCategorySharePercent,
|
||||
rule.MinPrimaryCategoryHours,
|
||||
rule.MaxDistinctCategoriesBeforeFlag,
|
||||
rule.IgnoredCategories,
|
||||
rule.MatchAwardCategoryAgainstTopCategories,
|
||||
rule.FlagIfAwardCategoryNotInTopX,
|
||||
rule.FlagIfCategorySpreadTooWide,
|
||||
rule.FlagIfNoCategoryContextAvailable,
|
||||
rule.MinValue,
|
||||
rule.MaxValue);
|
||||
|
||||
private static TrackingMetricRuleSetting ToTrackingMetricRuleSetting(AdminTrackingMetricRuleDto rule) =>
|
||||
new(
|
||||
rule.Key,
|
||||
rule.Label,
|
||||
rule.Enabled,
|
||||
rule.SourceSupport,
|
||||
rule.Description,
|
||||
rule.RequiredForAutoClassification,
|
||||
rule.ShowInReview,
|
||||
rule.ShowInAdminSummary,
|
||||
rule.ManualOverrideAllowed,
|
||||
TrackingRulesSettings.NormalizeWindowKey(rule.WindowKey, TrackingRulesSettings.Window30d),
|
||||
(rule.AutoSupportedWindowKeys ?? []).Select(item => TrackingRulesSettings.NormalizeWindowKey(item, TrackingRulesSettings.Window30d)).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(),
|
||||
string.IsNullOrWhiteSpace(rule.ProviderFieldKey) ? null : rule.ProviderFieldKey.Trim(),
|
||||
rule.TopCount,
|
||||
rule.MinPrimaryCategorySharePercent,
|
||||
rule.MinPrimaryCategoryHours,
|
||||
rule.MaxDistinctCategoriesBeforeFlag,
|
||||
(rule.IgnoredCategories ?? []).Select(item => item.Trim()).Where(item => !string.IsNullOrWhiteSpace(item)).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(),
|
||||
rule.MatchAwardCategoryAgainstTopCategories,
|
||||
rule.FlagIfAwardCategoryNotInTopX,
|
||||
rule.FlagIfCategorySpreadTooWide,
|
||||
rule.FlagIfNoCategoryContextAvailable,
|
||||
rule.MinValue,
|
||||
rule.MaxValue);
|
||||
|
||||
private static AdminTrackingFlagRuleDto ToTrackingFlagRuleDto(TrackingFlagRuleSetting rule) =>
|
||||
new(
|
||||
rule.Key,
|
||||
rule.Label,
|
||||
rule.Enabled,
|
||||
rule.Severity,
|
||||
rule.Description,
|
||||
rule.AutoTriggerEnabled,
|
||||
rule.RequiresManualReview,
|
||||
rule.BlocksApproval,
|
||||
rule.AdminNoteRequiredOnOverride);
|
||||
|
||||
private static TrackingFlagRuleSetting ToTrackingFlagRuleSetting(AdminTrackingFlagRuleDto rule) =>
|
||||
new(
|
||||
rule.Key,
|
||||
rule.Label,
|
||||
rule.Enabled,
|
||||
rule.Severity,
|
||||
rule.Description,
|
||||
rule.AutoTriggerEnabled,
|
||||
rule.RequiresManualReview,
|
||||
rule.BlocksApproval,
|
||||
rule.AdminNoteRequiredOnOverride);
|
||||
|
||||
private static bool TryNormalizeTrackingSourceUrl(string? rawValue, out string normalizedValue, out string errorMessage)
|
||||
{
|
||||
normalizedValue = string.Empty;
|
||||
errorMessage = string.Empty;
|
||||
var trimmed = (rawValue ?? string.Empty).Trim();
|
||||
if (!Uri.TryCreate(trimmed, UriKind.Absolute, out var uri)
|
||||
|| (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
|
||||
{
|
||||
errorMessage = "Tracking Source URL muss eine absolute http/https-URL sein.";
|
||||
return false;
|
||||
}
|
||||
|
||||
normalizedValue = uri.GetLeftPart(UriPartial.Path).TrimEnd('/');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user