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:
@@ -8,6 +8,7 @@ public sealed class AwardsDbContext(DbContextOptions<AwardsDbContext> options) :
|
||||
public DbSet<Season> Seasons => Set<Season>();
|
||||
public DbSet<Category> Categories => Set<Category>();
|
||||
public DbSet<Candidate> Candidates => Set<Candidate>();
|
||||
public DbSet<StreamerIdentity> StreamerIdentities => Set<StreamerIdentity>();
|
||||
public DbSet<AwardResult> Results => Set<AwardResult>();
|
||||
public DbSet<Nomination> Nominations => Set<Nomination>();
|
||||
public DbSet<VoteBallot> VoteBallots => Set<VoteBallot>();
|
||||
@@ -30,6 +31,8 @@ public sealed class AwardsDbContext(DbContextOptions<AwardsDbContext> options) :
|
||||
entity.Property(item => item.Name).HasMaxLength(160);
|
||||
entity.Property(item => item.ShowStreamUrl).HasMaxLength(400);
|
||||
entity.Property(item => item.CurrentPhase).HasMaxLength(60);
|
||||
entity.Property(item => item.SubcategoryTemplatesJson).HasDefaultValue("[]");
|
||||
entity.Property(item => item.WorkflowRulesJson).HasDefaultValue("[]");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<SiteSettings>(entity =>
|
||||
@@ -53,14 +56,19 @@ public sealed class AwardsDbContext(DbContextOptions<AwardsDbContext> options) :
|
||||
entity.Property(item => item.TwitchClientSecret).HasMaxLength(180);
|
||||
entity.Property(item => item.TwitchRedirectUri).HasMaxLength(400);
|
||||
entity.Property(item => item.TwitchScope).HasMaxLength(300);
|
||||
entity.Property(item => item.SessionIdleTimeoutHours).HasDefaultValue(3);
|
||||
entity.Property(item => item.MaintenanceTitle).HasMaxLength(120);
|
||||
entity.Property(item => item.MaintenanceMessage).HasMaxLength(600);
|
||||
entity.Property(item => item.WorkflowRulesJson).HasDefaultValue("[]");
|
||||
entity.Property(item => item.TrackingRulesJson).HasDefaultValue("[]");
|
||||
entity.Property(item => item.ViewerStatsProviderBaseUrl).HasMaxLength(400);
|
||||
entity.Property(item => item.NominationLinkBlacklistJson).HasDefaultValue("[]");
|
||||
entity.Property(item => item.ClipSubmissionsEnabled).HasDefaultValue(false);
|
||||
entity.Property(item => item.ClipReviewEnabled).HasDefaultValue(true);
|
||||
entity.Property(item => item.ClipSubmissionDisabledMessage).HasMaxLength(240);
|
||||
entity.Property(item => item.ShowactApplicationsEnabled).HasDefaultValue(false);
|
||||
entity.Property(item => item.ShowactApplicationStartsAt);
|
||||
entity.Property(item => item.ShowactApplicationEndsAt);
|
||||
entity.Property(item => item.ShowactApplicationDisabledMessage).HasMaxLength(240);
|
||||
entity.Property(item => item.SponsorsVisible).HasDefaultValue(true);
|
||||
});
|
||||
@@ -93,13 +101,17 @@ public sealed class AwardsDbContext(DbContextOptions<AwardsDbContext> options) :
|
||||
entity.Property(item => item.GroupName).HasMaxLength(80);
|
||||
entity.Property(item => item.Name).HasMaxLength(120);
|
||||
entity.Property(item => item.Description).HasMaxLength(400);
|
||||
entity.Property(item => item.ViewerRangeMin);
|
||||
entity.Property(item => item.ViewerRangeMax);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Candidate>(entity =>
|
||||
{
|
||||
entity.HasIndex(item => item.StreamerIdentityId);
|
||||
entity.Property(item => item.DisplayName).HasMaxLength(120);
|
||||
entity.Property(item => item.ChannelSlug).HasMaxLength(120);
|
||||
entity.Property(item => item.Platform).HasMaxLength(40);
|
||||
entity.Property(item => item.NominationTally).HasDefaultValue(0);
|
||||
entity.Property(item => item.AcceptanceStatus).HasMaxLength(30).HasDefaultValue("open");
|
||||
entity.Property(item => item.AcceptanceNote).HasMaxLength(500);
|
||||
entity.Property(item => item.ClipCompilationUrl).HasMaxLength(500);
|
||||
@@ -108,15 +120,47 @@ public sealed class AwardsDbContext(DbContextOptions<AwardsDbContext> options) :
|
||||
entity.Property(item => item.ClipEmbedStatus).HasMaxLength(30).HasDefaultValue("unchecked");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<StreamerIdentity>(entity =>
|
||||
{
|
||||
entity.HasIndex(item => item.NormalizedKey).IsUnique();
|
||||
entity.Property(item => item.Platform).HasMaxLength(40);
|
||||
entity.Property(item => item.Login).HasMaxLength(120);
|
||||
entity.Property(item => item.NormalizedKey).HasMaxLength(180);
|
||||
entity.Property(item => item.DisplayName).HasMaxLength(120);
|
||||
entity.Property(item => item.ProfileUrl).HasMaxLength(500);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Nomination>(entity =>
|
||||
{
|
||||
entity.Property(item => item.CategoryGroupName).HasMaxLength(80).HasDefaultValue(string.Empty);
|
||||
entity.Property(item => item.SubmittedByTwitchId).HasMaxLength(120);
|
||||
entity.Property(item => item.CandidateText).HasMaxLength(120);
|
||||
entity.Property(item => item.StreamUrl).HasMaxLength(300);
|
||||
entity.Property(item => item.ResolvedChannel).HasMaxLength(120);
|
||||
entity.Property(item => item.ResolvedPlatform).HasMaxLength(40);
|
||||
entity.Property(item => item.HoursStreamed);
|
||||
entity.Property(item => item.HoursWatched);
|
||||
entity.Property(item => item.PeakViewers);
|
||||
entity.Property(item => item.FollowersGained);
|
||||
entity.Property(item => item.TrackerStatus).HasMaxLength(40).HasDefaultValue("pending");
|
||||
entity.Property(item => item.TrackingReviewStatus).HasMaxLength(30).HasDefaultValue("clear");
|
||||
entity.Property(item => item.TrackingFlagsJson).HasDefaultValue("[]");
|
||||
entity.Property(item => item.TrackingReviewNote).HasMaxLength(1000);
|
||||
entity.Property(item => item.TrackingReviewedByTwitchId).HasMaxLength(120);
|
||||
entity.Property(item => item.Status).HasMaxLength(20);
|
||||
entity.Property(item => item.ReviewNote).HasMaxLength(500);
|
||||
entity.Property(item => item.ReviewedByTwitchId).HasMaxLength(120);
|
||||
entity.HasIndex(item => new { item.SeasonId, item.Status });
|
||||
entity.HasIndex(item => new { item.SeasonId, item.CategoryGroupName, item.Status });
|
||||
entity.HasIndex(item => new { item.SeasonId, item.StreamerIdentityId, item.CategoryGroupName });
|
||||
entity.HasOne(item => item.Category)
|
||||
.WithMany()
|
||||
.HasForeignKey(item => item.CategoryId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
entity.HasOne(item => item.SuggestedCategory)
|
||||
.WithMany()
|
||||
.HasForeignKey(item => item.SuggestedCategoryId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<VoteBallot>(entity =>
|
||||
|
||||
Reference in New Issue
Block a user