From 1a74c0362175c30c4b82918f56c281e3d83d4cbe Mon Sep 17 00:00:00 2001 From: AzuTear Date: Mon, 29 Jun 2026 17:57:47 +0200 Subject: [PATCH] Restore production migration history --- Backend/Data/SeedCatalog.cs | 184 ++ .../20260617060000_InitialCreate.Designer.cs | 833 +++++++ .../20260617060000_InitialCreate.cs | 395 ++++ ...23112528_AddClipReviewWorkflow.Designer.cs | 1081 +++++++++ .../20260623112528_AddClipReviewWorkflow.cs | 42 + ...17_AddNominationReviewWorkflow.Designer.cs | 1099 +++++++++ ...60623113917_AddNominationReviewWorkflow.cs | 95 + ...228_AddAwardResultCategoryLock.Designer.cs | 1119 +++++++++ ...260623131228_AddAwardResultCategoryLock.cs | 93 + ...3152322_AddSeasonShowStreamUrl.Designer.cs | 1128 +++++++++ .../20260623152322_AddSeasonShowStreamUrl.cs | 58 + ...20260623153212_AddSiteSettings.Designer.cs | 1199 ++++++++++ .../20260623153212_AddSiteSettings.cs | 48 + ...ddPrivacyPolicyContentMetadata.Designer.cs | 1213 ++++++++++ ...3154438_AddPrivacyPolicyContentMetadata.cs | 58 + ...545_AddOperationalSiteSettings.Designer.cs | 1267 +++++++++++ ...260624065545_AddOperationalSiteSettings.cs | 143 ++ ...24133211_AddSeasonShowStartsAt.Designer.cs | 1274 +++++++++++ .../20260624133211_AddSeasonShowStartsAt.cs | 57 + ...624134432_AddClipCandidateLink.Designer.cs | 1289 +++++++++++ .../20260624134432_AddClipCandidateLink.cs | 54 + .../20260624150500_AddRiskFlagReviewNote.cs | 34 + .../20260624162000_AddRiskRulesJson.cs | 39 + .../20260625110000_AddFooterPageContent.cs | 54 + ...260625140614_AddTeamManagement.Designer.cs | 1428 ++++++++++++ .../20260625140614_AddTeamManagement.cs | 79 + ...3000_AddCreatorRoleAndTeamTwitchBinding.cs | 66 + ...VoteBallotSubmitterUniqueIndex.Designer.cs | 1471 ++++++++++++ ...13057_AddVoteBallotSubmitterUniqueIndex.cs | 135 ++ ..._AddCandidatePreparationFields.Designer.cs | 1527 +++++++++++++ ...627114301_AddCandidatePreparationFields.cs | 177 ++ ...627125128_AddWorkflowRulesJson.Designer.cs | 1534 +++++++++++++ .../20260627125128_AddWorkflowRulesJson.cs | 35 + ...AddOptionalClipFeatureSettings.Designer.cs | 1552 +++++++++++++ ...27131746_AddOptionalClipFeatureSettings.cs | 55 + ...729_AddNominationLinkBlacklist.Designer.cs | 1559 +++++++++++++ ...260627135729_AddNominationLinkBlacklist.cs | 36 + ...163650_AddClipAdminMenuVisible.Designer.cs | 1758 ++++++++++++++ .../20260627163650_AddClipAdminMenuVisible.cs | 152 ++ .../20260627171320_AddShareUrls.Designer.cs | 1768 +++++++++++++++ .../Migrations/20260627171320_AddShareUrls.cs | 47 + ...27174243_AddShowactDynamicForm.Designer.cs | 1777 +++++++++++++++ .../20260627174243_AddShowactDynamicForm.cs | 47 + ...091734_AddCategoryViewerRanges.Designer.cs | 1783 +++++++++++++++ .../20260628091734_AddCategoryViewerRanges.cs | 108 + ..._AddSessionIdleTimeoutSettings.Designer.cs | 1789 +++++++++++++++ ...628092717_AddSessionIdleTimeoutSettings.cs | 36 + ..._AddSeasonSubcategoryTemplates.Designer.cs | 1799 +++++++++++++++ ...628110302_AddSeasonSubcategoryTemplates.cs | 57 + ...NominationGroupTrackerIdentity.Designer.cs | 1936 ++++++++++++++++ ...15832_AddNominationGroupTrackerIdentity.cs | 388 ++++ ...AddShowactApplicationSchedule.Designer.cs} | 680 ++++-- ...628161949_AddShowactApplicationSchedule.cs | 46 + ...353_AddSeasonWorkflowRulesJson.Designer.cs | 2010 +++++++++++++++++ ...260628205353_AddSeasonWorkflowRulesJson.cs | 36 + .../20260629142009_InitialCleanBaseline.cs | 795 ------- Backend/Migrations/InitialCreate.manual.sql | 159 ++ Backend/Migrations/InitialCreate.sql | 257 +++ 58 files changed, 38994 insertions(+), 944 deletions(-) create mode 100644 Backend/Data/SeedCatalog.cs create mode 100644 Backend/Migrations/20260617060000_InitialCreate.Designer.cs create mode 100644 Backend/Migrations/20260617060000_InitialCreate.cs create mode 100644 Backend/Migrations/20260623112528_AddClipReviewWorkflow.Designer.cs create mode 100644 Backend/Migrations/20260623112528_AddClipReviewWorkflow.cs create mode 100644 Backend/Migrations/20260623113917_AddNominationReviewWorkflow.Designer.cs create mode 100644 Backend/Migrations/20260623113917_AddNominationReviewWorkflow.cs create mode 100644 Backend/Migrations/20260623131228_AddAwardResultCategoryLock.Designer.cs create mode 100644 Backend/Migrations/20260623131228_AddAwardResultCategoryLock.cs create mode 100644 Backend/Migrations/20260623152322_AddSeasonShowStreamUrl.Designer.cs create mode 100644 Backend/Migrations/20260623152322_AddSeasonShowStreamUrl.cs create mode 100644 Backend/Migrations/20260623153212_AddSiteSettings.Designer.cs create mode 100644 Backend/Migrations/20260623153212_AddSiteSettings.cs create mode 100644 Backend/Migrations/20260623154438_AddPrivacyPolicyContentMetadata.Designer.cs create mode 100644 Backend/Migrations/20260623154438_AddPrivacyPolicyContentMetadata.cs create mode 100644 Backend/Migrations/20260624065545_AddOperationalSiteSettings.Designer.cs create mode 100644 Backend/Migrations/20260624065545_AddOperationalSiteSettings.cs create mode 100644 Backend/Migrations/20260624133211_AddSeasonShowStartsAt.Designer.cs create mode 100644 Backend/Migrations/20260624133211_AddSeasonShowStartsAt.cs create mode 100644 Backend/Migrations/20260624134432_AddClipCandidateLink.Designer.cs create mode 100644 Backend/Migrations/20260624134432_AddClipCandidateLink.cs create mode 100644 Backend/Migrations/20260624150500_AddRiskFlagReviewNote.cs create mode 100644 Backend/Migrations/20260624162000_AddRiskRulesJson.cs create mode 100644 Backend/Migrations/20260625110000_AddFooterPageContent.cs create mode 100644 Backend/Migrations/20260625140614_AddTeamManagement.Designer.cs create mode 100644 Backend/Migrations/20260625140614_AddTeamManagement.cs create mode 100644 Backend/Migrations/20260625153000_AddCreatorRoleAndTeamTwitchBinding.cs create mode 100644 Backend/Migrations/20260626113057_AddVoteBallotSubmitterUniqueIndex.Designer.cs create mode 100644 Backend/Migrations/20260626113057_AddVoteBallotSubmitterUniqueIndex.cs create mode 100644 Backend/Migrations/20260627114301_AddCandidatePreparationFields.Designer.cs create mode 100644 Backend/Migrations/20260627114301_AddCandidatePreparationFields.cs create mode 100644 Backend/Migrations/20260627125128_AddWorkflowRulesJson.Designer.cs create mode 100644 Backend/Migrations/20260627125128_AddWorkflowRulesJson.cs create mode 100644 Backend/Migrations/20260627131746_AddOptionalClipFeatureSettings.Designer.cs create mode 100644 Backend/Migrations/20260627131746_AddOptionalClipFeatureSettings.cs create mode 100644 Backend/Migrations/20260627135729_AddNominationLinkBlacklist.Designer.cs create mode 100644 Backend/Migrations/20260627135729_AddNominationLinkBlacklist.cs create mode 100644 Backend/Migrations/20260627163650_AddClipAdminMenuVisible.Designer.cs create mode 100644 Backend/Migrations/20260627163650_AddClipAdminMenuVisible.cs create mode 100644 Backend/Migrations/20260627171320_AddShareUrls.Designer.cs create mode 100644 Backend/Migrations/20260627171320_AddShareUrls.cs create mode 100644 Backend/Migrations/20260627174243_AddShowactDynamicForm.Designer.cs create mode 100644 Backend/Migrations/20260627174243_AddShowactDynamicForm.cs create mode 100644 Backend/Migrations/20260628091734_AddCategoryViewerRanges.Designer.cs create mode 100644 Backend/Migrations/20260628091734_AddCategoryViewerRanges.cs create mode 100644 Backend/Migrations/20260628092717_AddSessionIdleTimeoutSettings.Designer.cs create mode 100644 Backend/Migrations/20260628092717_AddSessionIdleTimeoutSettings.cs create mode 100644 Backend/Migrations/20260628110302_AddSeasonSubcategoryTemplates.Designer.cs create mode 100644 Backend/Migrations/20260628110302_AddSeasonSubcategoryTemplates.cs create mode 100644 Backend/Migrations/20260628115832_AddNominationGroupTrackerIdentity.Designer.cs create mode 100644 Backend/Migrations/20260628115832_AddNominationGroupTrackerIdentity.cs rename Backend/Migrations/{20260629142009_InitialCleanBaseline.Designer.cs => 20260628161949_AddShowactApplicationSchedule.Designer.cs} (62%) create mode 100644 Backend/Migrations/20260628161949_AddShowactApplicationSchedule.cs create mode 100644 Backend/Migrations/20260628205353_AddSeasonWorkflowRulesJson.Designer.cs create mode 100644 Backend/Migrations/20260628205353_AddSeasonWorkflowRulesJson.cs delete mode 100644 Backend/Migrations/20260629142009_InitialCleanBaseline.cs create mode 100644 Backend/Migrations/InitialCreate.manual.sql create mode 100644 Backend/Migrations/InitialCreate.sql diff --git a/Backend/Data/SeedCatalog.cs b/Backend/Data/SeedCatalog.cs new file mode 100644 index 0000000..608eced --- /dev/null +++ b/Backend/Data/SeedCatalog.cs @@ -0,0 +1,184 @@ +namespace Backend.Data; + +using Backend.Services; + +internal sealed record AwardCategorySeed(string LegacyGroupName, string Name, string Slug, string Description, int SortOrder); +internal sealed record CandidateSeed(string CategorySlug, string DisplayName, string ChannelSlug, string Platform); +internal sealed record WinnerSeed(int Year, string CategorySlug, string DisplayName, string ChannelSlug, string Platform); +internal sealed record SiteFaqSeed(string Question, string Answer); +internal sealed record SiteSocialSeed(string Label, string Platform, string Url, string Icon); +internal sealed record SponsorSeed(string Name, string WebsiteUrl, string LogoUrl, string Description, string Tier, int SortOrder); + +internal static class SeedCatalog +{ + internal static readonly SeasonSubcategoryTemplateSetting[] DefaultSubcategoryTemplates = + [ + new("Hidden Star", "hidden-star", 1, 1, 20), + new("Rising Star", "rising-star", 2, 21, 60), + new("Shining Star", "shining-star", 3, 61, null), + ]; + + internal static readonly AwardCategorySeed[] AwardCategorySeeds = + [ + new("Main Awards", "VTuber des Jahres", "vtuber-des-jahres", "Die größte Auszeichnung des Jahres.", 1), + new("Discovery", "Best Newcomer", "best-newcomer", "Neue Stimmen, neue Welten und frische Energie für die Szene.", 2), + new("Creative", "Model & Design", "model-design", "Live2D, 3D, Outfit, Rigging und visuelle Identitaet.", 3), + new("Performance", "Gesang & Musik", "gesang-musik", "Songs, Covers, Konzerte und musikalische Highlights.", 4), + new("Gaming", "Best Gaming", "best-gaming", "Gameplay, Skill, Chaos und legendaere Gaming-Momente.", 5), + new("Entertainment", "Best Variety", "best-variety", "Talk, Comedy, Watchalongs und kreative Streamformate.", 6), + new("Community", "Community Liebling", "community-liebling", "Creator:innen, die ihre Community besonders stark verbinden.", 7), + new("Collab", "Best Collab & Duo", "best-collab-duo", "Gemeinsame Streams, Projekte und Duo-Dynamik.", 8), + ]; + + internal static readonly Dictionary LegacyCategorySlugMap = new(StringComparer.OrdinalIgnoreCase) + { + ["bestes-live-event"] = "best-newcomer", + ["clip-des-jahres"] = "model-design", + ["beste-community"] = "gesang-musik", + }; + + internal static readonly SiteFaqSeed[] SiteFaqSeeds = + [ + new( + "Wer darf nominiert werden?", + "Jede:r aktive deutschsprachige VTuber kann nominiert werden — unabhaengig von Follower-Zahl oder Plattform. Die Community schlaegt in der Nominierungsphase ihre Favorit:innen vor."), + new( + "Wie funktioniert das Voting?", + "Du meldest dich ausschließlich mit deinem Twitch-Account an — nur so kannst du teilnehmen. Das hält das Voting fair und bot-frei. Danach vergibst du pro Kategorie eine Stimme, änderbar bis zum Ende der Phase."), + new( + "Was kostet die Teilnahme?", + "Gar nichts. Nominieren, voten und die Show schauen ist komplett kostenlos — der VTuber Star Award ist ein Community-Event von Fans für Fans."), + new( + "Wann und wo findet die Award-Show statt?", + "Die große Live-Show wird von Jayuhime gehostet und auf Twitch & YouTube gestreamt. Den genauen Termin findest du im Countdown oben — sei live dabei, wenn die Stars gekürt werden!"), + new( + "Ich wurde nominiert — was nun?", + "Glückwunsch! Du erhältst eine Benachrichtigung mit allen Infos. Teile deine Nominierung gern mit deiner Community und ruf zum Voten auf — jede Stimme zählt."), + ]; + + internal static readonly SiteSocialSeed[] SiteSocialSeeds = + [ + new("Twitch", "twitch", "https://twitch.tv/jayuhime", "twitch"), + new("YouTube", "youtube", "https://youtube.com/c/Jayuhime", "youtube"), + new("X", "x", "https://x.com/jayuhime", "x"), + new("Instagram", "instagram", "https://instagram.com/jayuhime", "instagram"), + new("Discord", "discord", "https://discord.gg/jayuhime", "discord"), + ]; + + internal const string DefaultImprintContent = """ +Anbieter +VTuber Star Awards, vertreten durch Jayuhime. + +Kontakt +Nutze für organisatorische Fragen bitte die Kontaktseite oder die hinterlegte Kontaktadresse. + +Hinweis +Dieses Impressum ist ein redaktioneller Platzhalter und sollte vor dem Livegang mit den tatsächlichen Anbieterangaben ersetzt werden. +"""; + + internal const string DefaultContactContent = """ +Kontakt zum Award-Team +Du hast Fragen zur Nominierung, zum Voting, zu Clips oder zur Show? Schreib dem Team über die hinterlegte Kontaktseite. + +Datenschutzfragen +Für Datenschutzanfragen nutze bitte die Datenschutz-E-Mail aus diesem Footer. + +Community & Kooperationen +Social Links und Partneranfragen werden vom Admin-Team gepflegt und koordiniert. +"""; + + internal const string DefaultSponsorsContent = """ +Sponsoren & Partner +Hier können Sponsor:innen, Medienpartner, Community-Partner und Supporter des VTuber Star Awards vorgestellt werden. + +Partner werden im Rahmen der Show und auf den öffentlichen Kontaktflächen genannt, sobald sie final bestätigt sind. +"""; + + internal static readonly CandidateSeed[] CurrentCandidateSeeds = + [ + new("vtuber-des-jahres-shining-star", "Hoshimi Miyu", "@hoshimimiyu", "Twitch"), + new("vtuber-des-jahres-shining-star", "Kurainu", "@kurainu", "Twitch"), + new("vtuber-des-jahres-shining-star", "Shiro Ch.", "@shiroch", "Twitch"), + new("best-newcomer-hidden-star", "Aoi Sakura", "@aoisakura", "YouTube"), + new("best-newcomer-rising-star", "Nox Live", "@noxlive", "Twitch"), + new("model-design-shining-star", "Velvet Rei", "@velvetrei", "Twitch"), + new("model-design-rising-star", "Mochi Atelier", "@mochiatelier", "Cake"), + new("gesang-musik-shining-star", "Melo Diva", "@melodiva", "YouTube"), + new("gesang-musik-rising-star", "Yuki Stern", "@yukistern", "Twitch"), + new("best-gaming-shining-star", "Kurainu", "@kurainu", "Twitch"), + new("best-gaming-rising-star", "PixelPunk", "@pixelpunk", "Twitch"), + new("best-variety-shining-star", "Taro Chaos", "@tarochaos", "Twitch"), + new("best-variety-rising-star", "Kotaro Plays", "@kotaroplays", "YouTube"), + new("community-liebling-shining-star", "Shiro Ch.", "@shiroch", "Twitch"), + new("community-liebling-hidden-star", "Lumi", "@lumi_vt", "Cake"), + new("best-collab-duo-shining-star", "Akari & Nox", "@akari_vt", "Twitch"), + new("best-collab-duo-rising-star", "Mochi & Hana", "@mochi_mochi", "YouTube"), + ]; + + internal static readonly WinnerSeed[] WinnerSeeds = + [ + new(2025, "vtuber-des-jahres-shining-star", "Hoshimi Miyu", "@hoshimimiyu", "Twitch"), + new(2025, "best-newcomer-hidden-star", "Aoi Sakura", "@aoisakura", "YouTube"), + new(2025, "model-design-shining-star", "Velvet Rei", "@velvetrei", "Twitch"), + new(2025, "gesang-musik-shining-star", "Melo Diva", "@melodiva", "YouTube"), + new(2025, "best-gaming-shining-star", "Kurainu", "@kurainu", "Twitch"), + new(2025, "best-variety-shining-star", "Taro Chaos", "@tarochaos", "Twitch"), + new(2025, "community-liebling-shining-star", "Shiro Ch.", "@shiroch", "Twitch"), + new(2025, "best-collab-duo-shining-star", "Akari & Nox", "@akari_vt", "Cake"), + new(2024, "vtuber-des-jahres-shining-star", "Aoi Sakura", "@aoisakura", "YouTube"), + new(2024, "best-newcomer-hidden-star", "Lumi", "@lumi_vt", "Cake"), + new(2024, "model-design-rising-star", "Mochi Atelier", "@mochiatelier", "Cake"), + new(2024, "gesang-musik-rising-star", "Yuki Stern", "@yukistern", "Twitch"), + new(2024, "best-gaming-shining-star", "Starbyte", "@starbyte", "Twitch"), + new(2024, "best-variety-rising-star", "Kotaro Plays", "@kotaroplays", "YouTube"), + new(2024, "community-liebling-rising-star", "Moonrelay", "@moonrelay", "Twitch"), + new(2024, "best-collab-duo-rising-star", "Pixel & Kotaro", "@pixelpunk", "Twitch"), + new(2023, "vtuber-des-jahres-shining-star", "Akari Nova", "@akarinova", "Twitch"), + new(2023, "best-newcomer-rising-star", "Nox Live", "@noxlive", "Twitch"), + new(2023, "model-design-shining-star", "Rei Velvet", "@reivelvet", "YouTube"), + new(2023, "gesang-musik-shining-star", "Tenshi Vox", "@tenshivox", "Twitch"), + new(2023, "best-gaming-rising-star", "Bit Knight", "@bitknight", "Twitch"), + new(2023, "best-variety-hidden-star", "Hana Hearts", "@hanahearts", "Cake"), + new(2023, "community-liebling-rising-star", "Sora Blau", "@sorablau", "YouTube"), + new(2023, "best-collab-duo-rising-star", "Yuki & Melo", "@yukistern", "Twitch"), + ]; + + internal static readonly SponsorSeed[] DemoSponsorSeeds = + [ + new( + "HoshiForge Studio", + "https://hoshiforge.example", + "/demo/sponsors/hoshiforge-studio.svg", + "Branding-, Overlay- und Debuet-Visuals fuer VTuber-Projekte und Community-Events.", + "Presenting Sponsor", + 10), + new( + "NekoPixel Energy", + "https://nekopixel.example", + "/demo/sponsors/nekopixel-energy.svg", + "Community-fokussierter Drink-Partner fuer lange Showabende, Watchpartys und Creator-Collabs.", + "Gold Partner", + 20), + new( + "PrismLoop Audio", + "https://prismloop.example", + "/demo/sponsors/prismloop-audio.svg", + "Audio-Tools, Intro-Packs und Stream-Sounddesign fuer Live-Shows und Highlight-Clips.", + "Gold Partner", + 30), + new( + "CloudBeacon Hosting", + "https://cloudbeacon.example", + "/demo/sponsors/cloudbeacon-hosting.svg", + "Skalierbares Hosting fuer Voting, Landingpages und Event-Traffic rund um Showtage.", + "Tech Partner", + 40), + new( + "ChibiCanvas Market", + "https://chibicanvas.example", + "/demo/sponsors/chibicanvas-market.svg", + "Merch-, Sticker- und Artist-Marketplace mit Fokus auf VTuber, Emotes und Fanartikel.", + "Community Partner", + 50), + ]; +} diff --git a/Backend/Migrations/20260617060000_InitialCreate.Designer.cs b/Backend/Migrations/20260617060000_InitialCreate.Designer.cs new file mode 100644 index 0000000..64c99bd --- /dev/null +++ b/Backend/Migrations/20260617060000_InitialCreate.Designer.cs @@ -0,0 +1,833 @@ +// +using System; +using Backend.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backend.Migrations +{ + [DbContext(typeof(AwardsDbContext))] + [Migration("20260617060000_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("SeasonId"); + + b.ToTable("Results"); + + b.HasData( + new + { + Id = 1, + CandidateId = 8, + CategoryName = "VTuber des Jahres", + SeasonId = 2 + }, + new + { + Id = 2, + CandidateId = 9, + CategoryName = "Bestes Live Event", + SeasonId = 2 + }, + new + { + Id = 3, + CandidateId = 10, + CategoryName = "Clip des Jahres", + SeasonId = 2 + }, + new + { + Id = 4, + CandidateId = 11, + CategoryName = "VTuber des Jahres", + SeasonId = 3 + }, + new + { + Id = 5, + CandidateId = 12, + CategoryName = "Clip des Jahres", + SeasonId = 3 + }, + new + { + Id = 6, + CandidateId = 13, + CategoryName = "VTuber des Jahres", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ChannelSlug") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId"); + + b.ToTable("Candidates"); + + b.HasData( + new + { + Id = 1, + CategoryId = 1, + ChannelSlug = "@hoshimimiyu", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 2, + CategoryId = 1, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 3, + CategoryId = 1, + ChannelSlug = "@shiroch", + DisplayName = "Shiro Ch.", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 4, + CategoryId = 2, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 5, + CategoryId = 2, + ChannelSlug = "@aoisakura", + DisplayName = "Aoi Sakura Showcase", + Platform = "YouTube", + SeasonId = 1 + }, + new + { + Id = 6, + CategoryId = 3, + ChannelSlug = "@pyonkichikingdom", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 7, + CategoryId = 4, + ChannelSlug = "@moonrelay", + DisplayName = "Moonrelay", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 8, + CategoryId = 5, + ChannelSlug = "@hoshimimiyu", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 9, + CategoryId = 6, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 10, + CategoryId = 7, + ChannelSlug = "@pyonkichikingdom", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 11, + CategoryId = 8, + ChannelSlug = "@aoisakura", + DisplayName = "Aoi Sakura", + Platform = "YouTube", + SeasonId = 3 + }, + new + { + Id = 12, + CategoryId = 9, + ChannelSlug = "@starbyte", + DisplayName = "Starbyte", + Platform = "Twitch", + SeasonId = 3 + }, + new + { + Id = 13, + CategoryId = 10, + ChannelSlug = "@tenshivox", + DisplayName = "Tenshi Vox", + Platform = "Twitch", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MaxNomineesPerUser") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Slug") + .IsUnique(); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = 1, + Description = "Die groesste Auszeichnung des Jahres.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 1, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 2, + Description = "Events, Konzerte und 3D-Shows.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 1, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 3, + Description = "Der lustigste oder emotionalste Clip des Jahres.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 1, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 4, + Description = "Die aktivste und freundlichste Community.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "Beste Community", + SeasonId = 1, + Slug = "beste-community", + SortOrder = 4 + }, + new + { + Id = 5, + Description = "Archivkategorie 2025.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 2, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 6, + Description = "Archivkategorie 2025.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 2, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 7, + Description = "Archivkategorie 2025.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 2, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 8, + Description = "Archivkategorie 2024.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 3, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 9, + Description = "Archivkategorie 2024.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 3, + Slug = "clip-des-jahres", + SortOrder = 2 + }, + new + { + Id = 10, + Description = "Archivkategorie 2023.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 4, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CandidateText") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId"); + + b.ToTable("Nominations"); + + b.HasData( + new + { + Id = 1, + CandidateText = "Hoshimi Miyu", + CategoryId = 1, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 13, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + SubmittedByTwitchId = "twitch_hoshi" + }, + new + { + Id = 2, + CandidateText = "Kurainu 3D Live", + CategoryId = 2, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 14, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + SubmittedByTwitchId = "twitch_kurainu" + }); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrentPhase") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("IsCommunityOnly") + .HasColumnType("boolean"); + + b.Property("IsCurrent") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("NominationEndsAt") + .HasColumnType("date"); + + b.Property("NominationStartsAt") + .HasColumnType("date"); + + b.Property("ReviewEndsAt") + .HasColumnType("date"); + + b.Property("ReviewStartsAt") + .HasColumnType("date"); + + b.Property("ShowDate") + .HasColumnType("date"); + + b.Property("VotingEndsAt") + .HasColumnType("date"); + + b.Property("VotingStartsAt") + .HasColumnType("date"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("Seasons"); + + b.HasData( + new + { + Id = 1, + CurrentPhase = "Community Voting", + IsCommunityOnly = true, + IsCurrent = true, + Name = "VTuber Star Awards 2026", + NominationEndsAt = new DateOnly(2026, 5, 31), + NominationStartsAt = new DateOnly(2026, 5, 1), + ReviewEndsAt = new DateOnly(2026, 7, 10), + ReviewStartsAt = new DateOnly(2026, 7, 1), + ShowDate = new DateOnly(2026, 7, 20), + VotingEndsAt = new DateOnly(2026, 6, 30), + VotingStartsAt = new DateOnly(2026, 6, 1), + Year = 2026 + }, + new + { + Id = 2, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2025", + NominationEndsAt = new DateOnly(2025, 5, 31), + NominationStartsAt = new DateOnly(2025, 5, 1), + ReviewEndsAt = new DateOnly(2025, 7, 10), + ReviewStartsAt = new DateOnly(2025, 7, 1), + ShowDate = new DateOnly(2025, 7, 20), + VotingEndsAt = new DateOnly(2025, 6, 30), + VotingStartsAt = new DateOnly(2025, 6, 1), + Year = 2025 + }, + new + { + Id = 3, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2024", + NominationEndsAt = new DateOnly(2024, 5, 31), + NominationStartsAt = new DateOnly(2024, 5, 1), + ReviewEndsAt = new DateOnly(2024, 7, 10), + ReviewStartsAt = new DateOnly(2024, 7, 1), + ShowDate = new DateOnly(2024, 7, 20), + VotingEndsAt = new DateOnly(2024, 6, 30), + VotingStartsAt = new DateOnly(2024, 6, 1), + Year = 2024 + }, + new + { + Id = 4, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2023", + NominationEndsAt = new DateOnly(2023, 5, 31), + NominationStartsAt = new DateOnly(2023, 5, 1), + ReviewEndsAt = new DateOnly(2023, 7, 10), + ReviewStartsAt = new DateOnly(2023, 7, 1), + ShowDate = new DateOnly(2023, 7, 20), + VotingEndsAt = new DateOnly(2023, 6, 30), + VotingStartsAt = new DateOnly(2023, 6, 1), + Year = 2023 + }); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("VoteBallots"); + + b.HasData( + new + { + Id = 1, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_1" + }, + new + { + Id = 2, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 5, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_2" + }); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BallotId") + .HasColumnType("integer"); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BallotId"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.ToTable("VoteEntries"); + + b.HasData( + new + { + Id = 1, + BallotId = 1, + CandidateId = 1, + CategoryId = 1 + }, + new + { + Id = 2, + BallotId = 1, + CandidateId = 4, + CategoryId = 2 + }, + new + { + Id = 3, + BallotId = 2, + CandidateId = 2, + CategoryId = 1 + }, + new + { + Id = 4, + BallotId = 2, + CandidateId = 6, + CategoryId = 3 + }); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Results") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.HasOne("Backend.Domain.Category", "Category") + .WithMany("Candidates") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Categories") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId"); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.HasOne("Backend.Domain.VoteBallot", "Ballot") + .WithMany("Entries") + .HasForeignKey("BallotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ballot"); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Navigation("Candidates"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Navigation("Categories"); + + b.Navigation("Results"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Navigation("Entries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/Migrations/20260617060000_InitialCreate.cs b/Backend/Migrations/20260617060000_InitialCreate.cs new file mode 100644 index 0000000..d210e77 --- /dev/null +++ b/Backend/Migrations/20260617060000_InitialCreate.cs @@ -0,0 +1,395 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace Backend.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Seasons", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Year = table.Column(type: "integer", nullable: false), + Name = table.Column(type: "character varying(160)", maxLength: 160, nullable: false), + IsCurrent = table.Column(type: "boolean", nullable: false), + IsCommunityOnly = table.Column(type: "boolean", nullable: false), + CurrentPhase = table.Column(type: "character varying(60)", maxLength: 60, nullable: false), + NominationStartsAt = table.Column(type: "date", nullable: false), + NominationEndsAt = table.Column(type: "date", nullable: false), + VotingStartsAt = table.Column(type: "date", nullable: false), + VotingEndsAt = table.Column(type: "date", nullable: false), + ReviewStartsAt = table.Column(type: "date", nullable: false), + ReviewEndsAt = table.Column(type: "date", nullable: false), + ShowDate = table.Column(type: "date", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Seasons", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Categories", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + SeasonId = table.Column(type: "integer", nullable: false), + GroupName = table.Column(type: "character varying(80)", maxLength: 80, nullable: false), + Name = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), + Slug = table.Column(type: "text", nullable: false), + Description = table.Column(type: "character varying(400)", maxLength: 400, nullable: false), + SortOrder = table.Column(type: "integer", nullable: false), + MaxNomineesPerUser = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Categories", x => x.Id); + table.ForeignKey( + name: "FK_Categories_Seasons_SeasonId", + column: x => x.SeasonId, + principalTable: "Seasons", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "VoteBallots", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + SeasonId = table.Column(type: "integer", nullable: false), + SubmittedByTwitchId = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), + Status = table.Column(type: "character varying(30)", maxLength: 30, nullable: false), + SubmittedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_VoteBallots", x => x.Id); + table.ForeignKey( + name: "FK_VoteBallots_Seasons_SeasonId", + column: x => x.SeasonId, + principalTable: "Seasons", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Candidates", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + SeasonId = table.Column(type: "integer", nullable: false), + CategoryId = table.Column(type: "integer", nullable: false), + DisplayName = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), + ChannelSlug = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), + Platform = table.Column(type: "character varying(40)", maxLength: 40, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Candidates", x => x.Id); + table.ForeignKey( + name: "FK_Candidates_Categories_CategoryId", + column: x => x.CategoryId, + principalTable: "Categories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Candidates_Seasons_SeasonId", + column: x => x.SeasonId, + principalTable: "Seasons", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Nominations", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + SeasonId = table.Column(type: "integer", nullable: false), + CategoryId = table.Column(type: "integer", nullable: false), + SubmittedByTwitchId = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), + CandidateId = table.Column(type: "integer", nullable: true), + CandidateText = table.Column(type: "character varying(120)", maxLength: 120, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Nominations", x => x.Id); + table.ForeignKey( + name: "FK_Nominations_Candidates_CandidateId", + column: x => x.CandidateId, + principalTable: "Candidates", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Nominations_Categories_CategoryId", + column: x => x.CategoryId, + principalTable: "Categories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Nominations_Seasons_SeasonId", + column: x => x.SeasonId, + principalTable: "Seasons", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Results", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + SeasonId = table.Column(type: "integer", nullable: false), + CandidateId = table.Column(type: "integer", nullable: false), + CategoryName = table.Column(type: "character varying(120)", maxLength: 120, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Results", x => x.Id); + table.ForeignKey( + name: "FK_Results_Candidates_CandidateId", + column: x => x.CandidateId, + principalTable: "Candidates", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Results_Seasons_SeasonId", + column: x => x.SeasonId, + principalTable: "Seasons", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "VoteEntries", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + BallotId = table.Column(type: "integer", nullable: false), + CategoryId = table.Column(type: "integer", nullable: false), + CandidateId = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_VoteEntries", x => x.Id); + table.ForeignKey( + name: "FK_VoteEntries_Candidates_CandidateId", + column: x => x.CandidateId, + principalTable: "Candidates", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_VoteEntries_Categories_CategoryId", + column: x => x.CategoryId, + principalTable: "Categories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_VoteEntries_VoteBallots_BallotId", + column: x => x.BallotId, + principalTable: "VoteBallots", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.InsertData( + table: "Seasons", + columns: new[] { "Id", "CurrentPhase", "IsCommunityOnly", "IsCurrent", "Name", "NominationEndsAt", "NominationStartsAt", "ReviewEndsAt", "ReviewStartsAt", "ShowDate", "VotingEndsAt", "VotingStartsAt", "Year" }, + values: new object[,] + { + { 1, "Community Voting", true, true, "VTuber Star Awards 2026", new DateOnly(2026, 5, 31), new DateOnly(2026, 5, 1), new DateOnly(2026, 7, 10), new DateOnly(2026, 7, 1), new DateOnly(2026, 7, 20), new DateOnly(2026, 6, 30), new DateOnly(2026, 6, 1), 2026 }, + { 2, "Archived", true, false, "VTuber Star Awards 2025", new DateOnly(2025, 5, 31), new DateOnly(2025, 5, 1), new DateOnly(2025, 7, 10), new DateOnly(2025, 7, 1), new DateOnly(2025, 7, 20), new DateOnly(2025, 6, 30), new DateOnly(2025, 6, 1), 2025 }, + { 3, "Archived", true, false, "VTuber Star Awards 2024", new DateOnly(2024, 5, 31), new DateOnly(2024, 5, 1), new DateOnly(2024, 7, 10), new DateOnly(2024, 7, 1), new DateOnly(2024, 7, 20), new DateOnly(2024, 6, 30), new DateOnly(2024, 6, 1), 2024 }, + { 4, "Archived", true, false, "VTuber Star Awards 2023", new DateOnly(2023, 5, 31), new DateOnly(2023, 5, 1), new DateOnly(2023, 7, 10), new DateOnly(2023, 7, 1), new DateOnly(2023, 7, 20), new DateOnly(2023, 6, 30), new DateOnly(2023, 6, 1), 2023 } + }); + + migrationBuilder.InsertData( + table: "Categories", + columns: new[] { "Id", "Description", "GroupName", "MaxNomineesPerUser", "Name", "SeasonId", "Slug", "SortOrder" }, + values: new object[,] + { + { 1, "Die groesste Auszeichnung des Jahres.", "Main Awards", 3, "VTuber des Jahres", 1, "vtuber-des-jahres", 1 }, + { 2, "Events, Konzerte und 3D-Shows.", "Performance", 3, "Bestes Live Event", 1, "bestes-live-event", 2 }, + { 3, "Der lustigste oder emotionalste Clip des Jahres.", "Clips & Highlights", 3, "Clip des Jahres", 1, "clip-des-jahres", 3 }, + { 4, "Die aktivste und freundlichste Community.", "Main Awards", 3, "Beste Community", 1, "beste-community", 4 }, + { 5, "Archivkategorie 2025.", "Main Awards", 3, "VTuber des Jahres", 2, "vtuber-des-jahres", 1 }, + { 6, "Archivkategorie 2025.", "Performance", 3, "Bestes Live Event", 2, "bestes-live-event", 2 }, + { 7, "Archivkategorie 2025.", "Clips & Highlights", 3, "Clip des Jahres", 2, "clip-des-jahres", 3 }, + { 8, "Archivkategorie 2024.", "Main Awards", 3, "VTuber des Jahres", 3, "vtuber-des-jahres", 1 }, + { 9, "Archivkategorie 2024.", "Clips & Highlights", 3, "Clip des Jahres", 3, "clip-des-jahres", 2 }, + { 10, "Archivkategorie 2023.", "Main Awards", 3, "VTuber des Jahres", 4, "vtuber-des-jahres", 1 } + }); + + migrationBuilder.InsertData( + table: "VoteBallots", + columns: new[] { "Id", "SeasonId", "Status", "SubmittedAt", "SubmittedByTwitchId" }, + values: new object[,] + { + { 1, 1, "submitted", new DateTimeOffset(new DateTime(2026, 6, 11, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "twitch_vote_1" }, + { 2, 1, "submitted", new DateTimeOffset(new DateTime(2026, 6, 11, 12, 5, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "twitch_vote_2" } + }); + + migrationBuilder.InsertData( + table: "Candidates", + columns: new[] { "Id", "CategoryId", "ChannelSlug", "DisplayName", "Platform", "SeasonId" }, + values: new object[,] + { + { 1, 1, "@hoshimimiyu", "Hoshimi Miyu", "Twitch", 1 }, + { 2, 1, "@kurainu", "Kurainu", "Twitch", 1 }, + { 3, 1, "@shiroch", "Shiro Ch.", "Twitch", 1 }, + { 4, 2, "@kurainu", "Kurainu 3D Live", "Twitch", 1 }, + { 5, 2, "@aoisakura", "Aoi Sakura Showcase", "YouTube", 1 }, + { 6, 3, "@pyonkichikingdom", "Pyonkichi Kingdom", "Twitch", 1 }, + { 7, 4, "@moonrelay", "Moonrelay", "Twitch", 1 }, + { 8, 5, "@hoshimimiyu", "Hoshimi Miyu", "Twitch", 2 }, + { 9, 6, "@kurainu", "Kurainu 3D Live", "Twitch", 2 }, + { 10, 7, "@pyonkichikingdom", "Pyonkichi Kingdom", "Twitch", 2 }, + { 11, 8, "@aoisakura", "Aoi Sakura", "YouTube", 3 }, + { 12, 9, "@starbyte", "Starbyte", "Twitch", 3 }, + { 13, 10, "@tenshivox", "Tenshi Vox", "Twitch", 4 } + }); + + migrationBuilder.InsertData( + table: "Nominations", + columns: new[] { "Id", "CandidateId", "CandidateText", "CategoryId", "CreatedAt", "SeasonId", "SubmittedByTwitchId" }, + values: new object[,] + { + { 1, null, "Hoshimi Miyu", 1, new DateTimeOffset(new DateTime(2026, 6, 10, 13, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), 1, "twitch_hoshi" }, + { 2, null, "Kurainu 3D Live", 2, new DateTimeOffset(new DateTime(2026, 6, 10, 14, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), 1, "twitch_kurainu" } + }); + + migrationBuilder.InsertData( + table: "Results", + columns: new[] { "Id", "CandidateId", "CategoryName", "SeasonId" }, + values: new object[,] + { + { 1, 8, "VTuber des Jahres", 2 }, + { 2, 9, "Bestes Live Event", 2 }, + { 3, 10, "Clip des Jahres", 2 }, + { 4, 11, "VTuber des Jahres", 3 }, + { 5, 12, "Clip des Jahres", 3 }, + { 6, 13, "VTuber des Jahres", 4 } + }); + + migrationBuilder.InsertData( + table: "VoteEntries", + columns: new[] { "Id", "BallotId", "CandidateId", "CategoryId" }, + values: new object[,] + { + { 1, 1, 1, 1 }, + { 2, 1, 4, 2 }, + { 3, 2, 2, 1 }, + { 4, 2, 6, 3 } + }); + + migrationBuilder.CreateIndex( + name: "IX_Candidates_CategoryId", + table: "Candidates", + column: "CategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_Candidates_SeasonId", + table: "Candidates", + column: "SeasonId"); + + migrationBuilder.CreateIndex( + name: "IX_Categories_SeasonId_Slug", + table: "Categories", + columns: new[] { "SeasonId", "Slug" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Nominations_CandidateId", + table: "Nominations", + column: "CandidateId"); + + migrationBuilder.CreateIndex( + name: "IX_Nominations_CategoryId", + table: "Nominations", + column: "CategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_Nominations_SeasonId", + table: "Nominations", + column: "SeasonId"); + + migrationBuilder.CreateIndex( + name: "IX_Results_CandidateId", + table: "Results", + column: "CandidateId"); + + migrationBuilder.CreateIndex( + name: "IX_Results_SeasonId", + table: "Results", + column: "SeasonId"); + + migrationBuilder.CreateIndex( + name: "IX_Seasons_Year", + table: "Seasons", + column: "Year", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_VoteBallots_SeasonId", + table: "VoteBallots", + column: "SeasonId"); + + migrationBuilder.CreateIndex( + name: "IX_VoteEntries_BallotId", + table: "VoteEntries", + column: "BallotId"); + + migrationBuilder.CreateIndex( + name: "IX_VoteEntries_CandidateId", + table: "VoteEntries", + column: "CandidateId"); + + migrationBuilder.CreateIndex( + name: "IX_VoteEntries_CategoryId", + table: "VoteEntries", + column: "CategoryId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Nominations"); + + migrationBuilder.DropTable( + name: "Results"); + + migrationBuilder.DropTable( + name: "VoteEntries"); + + migrationBuilder.DropTable( + name: "Candidates"); + + migrationBuilder.DropTable( + name: "VoteBallots"); + + migrationBuilder.DropTable( + name: "Categories"); + + migrationBuilder.DropTable( + name: "Seasons"); + } + } +} diff --git a/Backend/Migrations/20260623112528_AddClipReviewWorkflow.Designer.cs b/Backend/Migrations/20260623112528_AddClipReviewWorkflow.Designer.cs new file mode 100644 index 0000000..7c4bf25 --- /dev/null +++ b/Backend/Migrations/20260623112528_AddClipReviewWorkflow.Designer.cs @@ -0,0 +1,1081 @@ +// +using System; +using Backend.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backend.Migrations +{ + [DbContext(typeof(AwardsDbContext))] + [Migration("20260623112528_AddClipReviewWorkflow")] + partial class AddClipReviewWorkflow + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backend.Domain.AdminAuditEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("AdminTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.HasKey("Id"); + + b.ToTable("AdminAuditEntries"); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("SeasonId"); + + b.ToTable("Results"); + + b.HasData( + new + { + Id = 1, + CandidateId = 8, + CategoryName = "VTuber des Jahres", + SeasonId = 2 + }, + new + { + Id = 2, + CandidateId = 9, + CategoryName = "Bestes Live Event", + SeasonId = 2 + }, + new + { + Id = 3, + CandidateId = 10, + CategoryName = "Clip des Jahres", + SeasonId = 2 + }, + new + { + Id = 4, + CandidateId = 11, + CategoryName = "VTuber des Jahres", + SeasonId = 3 + }, + new + { + Id = 5, + CandidateId = 12, + CategoryName = "Clip des Jahres", + SeasonId = 3 + }, + new + { + Id = 6, + CandidateId = 13, + CategoryName = "VTuber des Jahres", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ChannelSlug") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId"); + + b.ToTable("Candidates"); + + b.HasData( + new + { + Id = 1, + CategoryId = 1, + ChannelSlug = "@hoshimimiyu", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 2, + CategoryId = 1, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 3, + CategoryId = 1, + ChannelSlug = "@shiroch", + DisplayName = "Shiro Ch.", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 4, + CategoryId = 2, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 5, + CategoryId = 2, + ChannelSlug = "@aoisakura", + DisplayName = "Aoi Sakura Showcase", + Platform = "YouTube", + SeasonId = 1 + }, + new + { + Id = 6, + CategoryId = 3, + ChannelSlug = "@pyonkichikingdom", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 7, + CategoryId = 4, + ChannelSlug = "@moonrelay", + DisplayName = "Moonrelay", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 8, + CategoryId = 5, + ChannelSlug = "@hoshimimiyu", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 9, + CategoryId = 6, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 10, + CategoryId = 7, + ChannelSlug = "@pyonkichikingdom", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 11, + CategoryId = 8, + ChannelSlug = "@aoisakura", + DisplayName = "Aoi Sakura", + Platform = "YouTube", + SeasonId = 3 + }, + new + { + Id = 12, + CategoryId = 9, + ChannelSlug = "@starbyte", + DisplayName = "Starbyte", + Platform = "Twitch", + SeasonId = 3 + }, + new + { + Id = 13, + CategoryId = 10, + ChannelSlug = "@tenshivox", + DisplayName = "Tenshi Vox", + Platform = "Twitch", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MaxNomineesPerUser") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Slug") + .IsUnique(); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = 1, + Description = "Die groesste Auszeichnung des Jahres.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 1, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 2, + Description = "Events, Konzerte und 3D-Shows.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 1, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 3, + Description = "Der lustigste oder emotionalste Clip des Jahres.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 1, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 4, + Description = "Die aktivste und freundlichste Community.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "Beste Community", + SeasonId = 1, + Slug = "beste-community", + SortOrder = 4 + }, + new + { + Id = 5, + Description = "Archivkategorie 2025.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 2, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 6, + Description = "Archivkategorie 2025.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 2, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 7, + Description = "Archivkategorie 2025.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 2, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 8, + Description = "Archivkategorie 2024.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 3, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 9, + Description = "Archivkategorie 2024.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 3, + Slug = "clip-des-jahres", + SortOrder = 2 + }, + new + { + Id = 10, + Description = "Archivkategorie 2023.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 4, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ClipUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Creator") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("ClipSubmissions"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CandidateText") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId"); + + b.ToTable("Nominations"); + + b.HasData( + new + { + Id = 1, + CandidateText = "Hoshimi Miyu", + CategoryId = 1, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 13, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + SubmittedByTwitchId = "twitch_hoshi" + }, + new + { + Id = 2, + CandidateText = "Kurainu 3D Live", + CategoryId = 2, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 14, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + SubmittedByTwitchId = "twitch_kurainu" + }); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("TwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("RiskFlags"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrentPhase") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("IsCommunityOnly") + .HasColumnType("boolean"); + + b.Property("IsCurrent") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("NominationEndsAt") + .HasColumnType("date"); + + b.Property("NominationStartsAt") + .HasColumnType("date"); + + b.Property("ReviewEndsAt") + .HasColumnType("date"); + + b.Property("ReviewStartsAt") + .HasColumnType("date"); + + b.Property("ShowDate") + .HasColumnType("date"); + + b.Property("VotingEndsAt") + .HasColumnType("date"); + + b.Property("VotingStartsAt") + .HasColumnType("date"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("Seasons"); + + b.HasData( + new + { + Id = 1, + CurrentPhase = "Community Voting", + IsCommunityOnly = true, + IsCurrent = true, + Name = "VTuber Star Awards 2026", + NominationEndsAt = new DateOnly(2026, 5, 31), + NominationStartsAt = new DateOnly(2026, 5, 1), + ReviewEndsAt = new DateOnly(2026, 7, 10), + ReviewStartsAt = new DateOnly(2026, 7, 1), + ShowDate = new DateOnly(2026, 7, 20), + VotingEndsAt = new DateOnly(2026, 6, 30), + VotingStartsAt = new DateOnly(2026, 6, 1), + Year = 2026 + }, + new + { + Id = 2, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2025", + NominationEndsAt = new DateOnly(2025, 5, 31), + NominationStartsAt = new DateOnly(2025, 5, 1), + ReviewEndsAt = new DateOnly(2025, 7, 10), + ReviewStartsAt = new DateOnly(2025, 7, 1), + ShowDate = new DateOnly(2025, 7, 20), + VotingEndsAt = new DateOnly(2025, 6, 30), + VotingStartsAt = new DateOnly(2025, 6, 1), + Year = 2025 + }, + new + { + Id = 3, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2024", + NominationEndsAt = new DateOnly(2024, 5, 31), + NominationStartsAt = new DateOnly(2024, 5, 1), + ReviewEndsAt = new DateOnly(2024, 7, 10), + ReviewStartsAt = new DateOnly(2024, 7, 1), + ShowDate = new DateOnly(2024, 7, 20), + VotingEndsAt = new DateOnly(2024, 6, 30), + VotingStartsAt = new DateOnly(2024, 6, 1), + Year = 2024 + }, + new + { + Id = 4, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2023", + NominationEndsAt = new DateOnly(2023, 5, 31), + NominationStartsAt = new DateOnly(2023, 5, 1), + ReviewEndsAt = new DateOnly(2023, 7, 10), + ReviewStartsAt = new DateOnly(2023, 7, 1), + ShowDate = new DateOnly(2023, 7, 20), + VotingEndsAt = new DateOnly(2023, 6, 30), + VotingStartsAt = new DateOnly(2023, 6, 1), + Year = 2023 + }); + }); + + modelBuilder.Entity("Backend.Domain.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SessionToken") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SessionToken") + .IsUnique(); + + b.ToTable("UserSessions"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("VoteBallots"); + + b.HasData( + new + { + Id = 1, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_1" + }, + new + { + Id = 2, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 5, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_2" + }); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BallotId") + .HasColumnType("integer"); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BallotId"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.ToTable("VoteEntries"); + + b.HasData( + new + { + Id = 1, + BallotId = 1, + CandidateId = 1, + CategoryId = 1 + }, + new + { + Id = 2, + BallotId = 1, + CandidateId = 4, + CategoryId = 2 + }, + new + { + Id = 3, + BallotId = 2, + CandidateId = 2, + CategoryId = 1 + }, + new + { + Id = 4, + BallotId = 2, + CandidateId = 6, + CategoryId = 3 + }); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Results") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.HasOne("Backend.Domain.Category", "Category") + .WithMany("Candidates") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Categories") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId"); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.HasOne("Backend.Domain.VoteBallot", "Ballot") + .WithMany("Entries") + .HasForeignKey("BallotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ballot"); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Navigation("Candidates"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Navigation("Categories"); + + b.Navigation("Results"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Navigation("Entries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/Migrations/20260623112528_AddClipReviewWorkflow.cs b/Backend/Migrations/20260623112528_AddClipReviewWorkflow.cs new file mode 100644 index 0000000..67917b0 --- /dev/null +++ b/Backend/Migrations/20260623112528_AddClipReviewWorkflow.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backend.Migrations +{ + /// + public partial class AddClipReviewWorkflow : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql( + """ + ALTER TABLE IF EXISTS "ClipSubmissions" + ADD COLUMN IF NOT EXISTS "ReviewNote" character varying(500) NULL; + + ALTER TABLE IF EXISTS "ClipSubmissions" + ADD COLUMN IF NOT EXISTS "ReviewedByTwitchId" character varying(120) NULL; + + ALTER TABLE IF EXISTS "ClipSubmissions" + ADD COLUMN IF NOT EXISTS "ReviewedAt" timestamp with time zone NULL; + """); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql( + """ + ALTER TABLE IF EXISTS "ClipSubmissions" + DROP COLUMN IF EXISTS "ReviewNote"; + + ALTER TABLE IF EXISTS "ClipSubmissions" + DROP COLUMN IF EXISTS "ReviewedByTwitchId"; + + ALTER TABLE IF EXISTS "ClipSubmissions" + DROP COLUMN IF EXISTS "ReviewedAt"; + """); + } + } +} diff --git a/Backend/Migrations/20260623113917_AddNominationReviewWorkflow.Designer.cs b/Backend/Migrations/20260623113917_AddNominationReviewWorkflow.Designer.cs new file mode 100644 index 0000000..5cd66df --- /dev/null +++ b/Backend/Migrations/20260623113917_AddNominationReviewWorkflow.Designer.cs @@ -0,0 +1,1099 @@ +// +using System; +using Backend.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backend.Migrations +{ + [DbContext(typeof(AwardsDbContext))] + [Migration("20260623113917_AddNominationReviewWorkflow")] + partial class AddNominationReviewWorkflow + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backend.Domain.AdminAuditEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("AdminTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.HasKey("Id"); + + b.ToTable("AdminAuditEntries"); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("SeasonId"); + + b.ToTable("Results"); + + b.HasData( + new + { + Id = 1, + CandidateId = 8, + CategoryName = "VTuber des Jahres", + SeasonId = 2 + }, + new + { + Id = 2, + CandidateId = 9, + CategoryName = "Bestes Live Event", + SeasonId = 2 + }, + new + { + Id = 3, + CandidateId = 10, + CategoryName = "Clip des Jahres", + SeasonId = 2 + }, + new + { + Id = 4, + CandidateId = 11, + CategoryName = "VTuber des Jahres", + SeasonId = 3 + }, + new + { + Id = 5, + CandidateId = 12, + CategoryName = "Clip des Jahres", + SeasonId = 3 + }, + new + { + Id = 6, + CandidateId = 13, + CategoryName = "VTuber des Jahres", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ChannelSlug") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId"); + + b.ToTable("Candidates"); + + b.HasData( + new + { + Id = 1, + CategoryId = 1, + ChannelSlug = "@hoshimimiyu", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 2, + CategoryId = 1, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 3, + CategoryId = 1, + ChannelSlug = "@shiroch", + DisplayName = "Shiro Ch.", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 4, + CategoryId = 2, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 5, + CategoryId = 2, + ChannelSlug = "@aoisakura", + DisplayName = "Aoi Sakura Showcase", + Platform = "YouTube", + SeasonId = 1 + }, + new + { + Id = 6, + CategoryId = 3, + ChannelSlug = "@pyonkichikingdom", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 7, + CategoryId = 4, + ChannelSlug = "@moonrelay", + DisplayName = "Moonrelay", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 8, + CategoryId = 5, + ChannelSlug = "@hoshimimiyu", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 9, + CategoryId = 6, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 10, + CategoryId = 7, + ChannelSlug = "@pyonkichikingdom", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 11, + CategoryId = 8, + ChannelSlug = "@aoisakura", + DisplayName = "Aoi Sakura", + Platform = "YouTube", + SeasonId = 3 + }, + new + { + Id = 12, + CategoryId = 9, + ChannelSlug = "@starbyte", + DisplayName = "Starbyte", + Platform = "Twitch", + SeasonId = 3 + }, + new + { + Id = 13, + CategoryId = 10, + ChannelSlug = "@tenshivox", + DisplayName = "Tenshi Vox", + Platform = "Twitch", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MaxNomineesPerUser") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Slug") + .IsUnique(); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = 1, + Description = "Die groesste Auszeichnung des Jahres.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 1, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 2, + Description = "Events, Konzerte und 3D-Shows.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 1, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 3, + Description = "Der lustigste oder emotionalste Clip des Jahres.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 1, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 4, + Description = "Die aktivste und freundlichste Community.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "Beste Community", + SeasonId = 1, + Slug = "beste-community", + SortOrder = 4 + }, + new + { + Id = 5, + Description = "Archivkategorie 2025.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 2, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 6, + Description = "Archivkategorie 2025.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 2, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 7, + Description = "Archivkategorie 2025.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 2, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 8, + Description = "Archivkategorie 2024.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 3, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 9, + Description = "Archivkategorie 2024.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 3, + Slug = "clip-des-jahres", + SortOrder = 2 + }, + new + { + Id = 10, + Description = "Archivkategorie 2023.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 4, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ClipUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Creator") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("ClipSubmissions"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CandidateText") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("Nominations"); + + b.HasData( + new + { + Id = 1, + CandidateText = "Hoshimi Miyu", + CategoryId = 1, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 13, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_hoshi" + }, + new + { + Id = 2, + CandidateText = "Kurainu 3D Live", + CategoryId = 2, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 14, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_kurainu" + }); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("TwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("RiskFlags"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrentPhase") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("IsCommunityOnly") + .HasColumnType("boolean"); + + b.Property("IsCurrent") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("NominationEndsAt") + .HasColumnType("date"); + + b.Property("NominationStartsAt") + .HasColumnType("date"); + + b.Property("ReviewEndsAt") + .HasColumnType("date"); + + b.Property("ReviewStartsAt") + .HasColumnType("date"); + + b.Property("ShowDate") + .HasColumnType("date"); + + b.Property("VotingEndsAt") + .HasColumnType("date"); + + b.Property("VotingStartsAt") + .HasColumnType("date"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("Seasons"); + + b.HasData( + new + { + Id = 1, + CurrentPhase = "Community Voting", + IsCommunityOnly = true, + IsCurrent = true, + Name = "VTuber Star Awards 2026", + NominationEndsAt = new DateOnly(2026, 5, 31), + NominationStartsAt = new DateOnly(2026, 5, 1), + ReviewEndsAt = new DateOnly(2026, 7, 10), + ReviewStartsAt = new DateOnly(2026, 7, 1), + ShowDate = new DateOnly(2026, 7, 20), + VotingEndsAt = new DateOnly(2026, 6, 30), + VotingStartsAt = new DateOnly(2026, 6, 1), + Year = 2026 + }, + new + { + Id = 2, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2025", + NominationEndsAt = new DateOnly(2025, 5, 31), + NominationStartsAt = new DateOnly(2025, 5, 1), + ReviewEndsAt = new DateOnly(2025, 7, 10), + ReviewStartsAt = new DateOnly(2025, 7, 1), + ShowDate = new DateOnly(2025, 7, 20), + VotingEndsAt = new DateOnly(2025, 6, 30), + VotingStartsAt = new DateOnly(2025, 6, 1), + Year = 2025 + }, + new + { + Id = 3, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2024", + NominationEndsAt = new DateOnly(2024, 5, 31), + NominationStartsAt = new DateOnly(2024, 5, 1), + ReviewEndsAt = new DateOnly(2024, 7, 10), + ReviewStartsAt = new DateOnly(2024, 7, 1), + ShowDate = new DateOnly(2024, 7, 20), + VotingEndsAt = new DateOnly(2024, 6, 30), + VotingStartsAt = new DateOnly(2024, 6, 1), + Year = 2024 + }, + new + { + Id = 4, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2023", + NominationEndsAt = new DateOnly(2023, 5, 31), + NominationStartsAt = new DateOnly(2023, 5, 1), + ReviewEndsAt = new DateOnly(2023, 7, 10), + ReviewStartsAt = new DateOnly(2023, 7, 1), + ShowDate = new DateOnly(2023, 7, 20), + VotingEndsAt = new DateOnly(2023, 6, 30), + VotingStartsAt = new DateOnly(2023, 6, 1), + Year = 2023 + }); + }); + + modelBuilder.Entity("Backend.Domain.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SessionToken") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SessionToken") + .IsUnique(); + + b.ToTable("UserSessions"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("VoteBallots"); + + b.HasData( + new + { + Id = 1, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_1" + }, + new + { + Id = 2, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 5, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_2" + }); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BallotId") + .HasColumnType("integer"); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BallotId"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.ToTable("VoteEntries"); + + b.HasData( + new + { + Id = 1, + BallotId = 1, + CandidateId = 1, + CategoryId = 1 + }, + new + { + Id = 2, + BallotId = 1, + CandidateId = 4, + CategoryId = 2 + }, + new + { + Id = 3, + BallotId = 2, + CandidateId = 2, + CategoryId = 1 + }, + new + { + Id = 4, + BallotId = 2, + CandidateId = 6, + CategoryId = 3 + }); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Results") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.HasOne("Backend.Domain.Category", "Category") + .WithMany("Candidates") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Categories") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId"); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.HasOne("Backend.Domain.VoteBallot", "Ballot") + .WithMany("Entries") + .HasForeignKey("BallotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ballot"); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Navigation("Candidates"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Navigation("Categories"); + + b.Navigation("Results"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Navigation("Entries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/Migrations/20260623113917_AddNominationReviewWorkflow.cs b/Backend/Migrations/20260623113917_AddNominationReviewWorkflow.cs new file mode 100644 index 0000000..662309e --- /dev/null +++ b/Backend/Migrations/20260623113917_AddNominationReviewWorkflow.cs @@ -0,0 +1,95 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backend.Migrations +{ + /// + public partial class AddNominationReviewWorkflow : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Nominations_SeasonId", + table: "Nominations"); + + migrationBuilder.AddColumn( + name: "ReviewNote", + table: "Nominations", + type: "character varying(500)", + maxLength: 500, + nullable: true); + + migrationBuilder.AddColumn( + name: "ReviewedAt", + table: "Nominations", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "ReviewedByTwitchId", + table: "Nominations", + type: "character varying(120)", + maxLength: 120, + nullable: true); + + migrationBuilder.AddColumn( + name: "Status", + table: "Nominations", + type: "character varying(20)", + maxLength: 20, + nullable: false, + defaultValue: ""); + + migrationBuilder.UpdateData( + table: "Nominations", + keyColumn: "Id", + keyValue: 1, + columns: new[] { "ReviewNote", "ReviewedAt", "ReviewedByTwitchId", "Status" }, + values: new object[] { null, null, null, "pending" }); + + migrationBuilder.UpdateData( + table: "Nominations", + keyColumn: "Id", + keyValue: 2, + columns: new[] { "ReviewNote", "ReviewedAt", "ReviewedByTwitchId", "Status" }, + values: new object[] { null, null, null, "pending" }); + + migrationBuilder.CreateIndex( + name: "IX_Nominations_SeasonId_Status", + table: "Nominations", + columns: new[] { "SeasonId", "Status" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Nominations_SeasonId_Status", + table: "Nominations"); + + migrationBuilder.DropColumn( + name: "ReviewNote", + table: "Nominations"); + + migrationBuilder.DropColumn( + name: "ReviewedAt", + table: "Nominations"); + + migrationBuilder.DropColumn( + name: "ReviewedByTwitchId", + table: "Nominations"); + + migrationBuilder.DropColumn( + name: "Status", + table: "Nominations"); + + migrationBuilder.CreateIndex( + name: "IX_Nominations_SeasonId", + table: "Nominations", + column: "SeasonId"); + } + } +} diff --git a/Backend/Migrations/20260623131228_AddAwardResultCategoryLock.Designer.cs b/Backend/Migrations/20260623131228_AddAwardResultCategoryLock.Designer.cs new file mode 100644 index 0000000..79467f3 --- /dev/null +++ b/Backend/Migrations/20260623131228_AddAwardResultCategoryLock.Designer.cs @@ -0,0 +1,1119 @@ +// +using System; +using Backend.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backend.Migrations +{ + [DbContext(typeof(AwardsDbContext))] + [Migration("20260623131228_AddAwardResultCategoryLock")] + partial class AddAwardResultCategoryLock + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backend.Domain.AdminAuditEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("AdminTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.HasKey("Id"); + + b.ToTable("AdminAuditEntries"); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CategoryName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "CategoryId") + .IsUnique(); + + b.ToTable("Results"); + + b.HasData( + new + { + Id = 1, + CandidateId = 8, + CategoryId = 5, + CategoryName = "VTuber des Jahres", + SeasonId = 2 + }, + new + { + Id = 2, + CandidateId = 9, + CategoryId = 6, + CategoryName = "Bestes Live Event", + SeasonId = 2 + }, + new + { + Id = 3, + CandidateId = 10, + CategoryId = 7, + CategoryName = "Clip des Jahres", + SeasonId = 2 + }, + new + { + Id = 4, + CandidateId = 11, + CategoryId = 8, + CategoryName = "VTuber des Jahres", + SeasonId = 3 + }, + new + { + Id = 5, + CandidateId = 12, + CategoryId = 9, + CategoryName = "Clip des Jahres", + SeasonId = 3 + }, + new + { + Id = 6, + CandidateId = 13, + CategoryId = 10, + CategoryName = "VTuber des Jahres", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ChannelSlug") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId"); + + b.ToTable("Candidates"); + + b.HasData( + new + { + Id = 1, + CategoryId = 1, + ChannelSlug = "@hoshimimiyu", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 2, + CategoryId = 1, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 3, + CategoryId = 1, + ChannelSlug = "@shiroch", + DisplayName = "Shiro Ch.", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 4, + CategoryId = 2, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 5, + CategoryId = 2, + ChannelSlug = "@aoisakura", + DisplayName = "Aoi Sakura Showcase", + Platform = "YouTube", + SeasonId = 1 + }, + new + { + Id = 6, + CategoryId = 3, + ChannelSlug = "@pyonkichikingdom", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 7, + CategoryId = 4, + ChannelSlug = "@moonrelay", + DisplayName = "Moonrelay", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 8, + CategoryId = 5, + ChannelSlug = "@hoshimimiyu", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 9, + CategoryId = 6, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 10, + CategoryId = 7, + ChannelSlug = "@pyonkichikingdom", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 11, + CategoryId = 8, + ChannelSlug = "@aoisakura", + DisplayName = "Aoi Sakura", + Platform = "YouTube", + SeasonId = 3 + }, + new + { + Id = 12, + CategoryId = 9, + ChannelSlug = "@starbyte", + DisplayName = "Starbyte", + Platform = "Twitch", + SeasonId = 3 + }, + new + { + Id = 13, + CategoryId = 10, + ChannelSlug = "@tenshivox", + DisplayName = "Tenshi Vox", + Platform = "Twitch", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MaxNomineesPerUser") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Slug") + .IsUnique(); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = 1, + Description = "Die groesste Auszeichnung des Jahres.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 1, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 2, + Description = "Events, Konzerte und 3D-Shows.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 1, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 3, + Description = "Der lustigste oder emotionalste Clip des Jahres.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 1, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 4, + Description = "Die aktivste und freundlichste Community.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "Beste Community", + SeasonId = 1, + Slug = "beste-community", + SortOrder = 4 + }, + new + { + Id = 5, + Description = "Archivkategorie 2025.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 2, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 6, + Description = "Archivkategorie 2025.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 2, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 7, + Description = "Archivkategorie 2025.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 2, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 8, + Description = "Archivkategorie 2024.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 3, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 9, + Description = "Archivkategorie 2024.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 3, + Slug = "clip-des-jahres", + SortOrder = 2 + }, + new + { + Id = 10, + Description = "Archivkategorie 2023.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 4, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ClipUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Creator") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("ClipSubmissions"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CandidateText") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("Nominations"); + + b.HasData( + new + { + Id = 1, + CandidateText = "Hoshimi Miyu", + CategoryId = 1, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 13, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_hoshi" + }, + new + { + Id = 2, + CandidateText = "Kurainu 3D Live", + CategoryId = 2, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 14, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_kurainu" + }); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("TwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("RiskFlags"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrentPhase") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("IsCommunityOnly") + .HasColumnType("boolean"); + + b.Property("IsCurrent") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("NominationEndsAt") + .HasColumnType("date"); + + b.Property("NominationStartsAt") + .HasColumnType("date"); + + b.Property("ReviewEndsAt") + .HasColumnType("date"); + + b.Property("ReviewStartsAt") + .HasColumnType("date"); + + b.Property("ShowDate") + .HasColumnType("date"); + + b.Property("VotingEndsAt") + .HasColumnType("date"); + + b.Property("VotingStartsAt") + .HasColumnType("date"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("Seasons"); + + b.HasData( + new + { + Id = 1, + CurrentPhase = "Community Voting", + IsCommunityOnly = true, + IsCurrent = true, + Name = "VTuber Star Awards 2026", + NominationEndsAt = new DateOnly(2026, 5, 31), + NominationStartsAt = new DateOnly(2026, 5, 1), + ReviewEndsAt = new DateOnly(2026, 7, 10), + ReviewStartsAt = new DateOnly(2026, 7, 1), + ShowDate = new DateOnly(2026, 7, 20), + VotingEndsAt = new DateOnly(2026, 6, 30), + VotingStartsAt = new DateOnly(2026, 6, 1), + Year = 2026 + }, + new + { + Id = 2, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2025", + NominationEndsAt = new DateOnly(2025, 5, 31), + NominationStartsAt = new DateOnly(2025, 5, 1), + ReviewEndsAt = new DateOnly(2025, 7, 10), + ReviewStartsAt = new DateOnly(2025, 7, 1), + ShowDate = new DateOnly(2025, 7, 20), + VotingEndsAt = new DateOnly(2025, 6, 30), + VotingStartsAt = new DateOnly(2025, 6, 1), + Year = 2025 + }, + new + { + Id = 3, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2024", + NominationEndsAt = new DateOnly(2024, 5, 31), + NominationStartsAt = new DateOnly(2024, 5, 1), + ReviewEndsAt = new DateOnly(2024, 7, 10), + ReviewStartsAt = new DateOnly(2024, 7, 1), + ShowDate = new DateOnly(2024, 7, 20), + VotingEndsAt = new DateOnly(2024, 6, 30), + VotingStartsAt = new DateOnly(2024, 6, 1), + Year = 2024 + }, + new + { + Id = 4, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2023", + NominationEndsAt = new DateOnly(2023, 5, 31), + NominationStartsAt = new DateOnly(2023, 5, 1), + ReviewEndsAt = new DateOnly(2023, 7, 10), + ReviewStartsAt = new DateOnly(2023, 7, 1), + ShowDate = new DateOnly(2023, 7, 20), + VotingEndsAt = new DateOnly(2023, 6, 30), + VotingStartsAt = new DateOnly(2023, 6, 1), + Year = 2023 + }); + }); + + modelBuilder.Entity("Backend.Domain.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SessionToken") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SessionToken") + .IsUnique(); + + b.ToTable("UserSessions"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("VoteBallots"); + + b.HasData( + new + { + Id = 1, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_1" + }, + new + { + Id = 2, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 5, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_2" + }); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BallotId") + .HasColumnType("integer"); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BallotId"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.ToTable("VoteEntries"); + + b.HasData( + new + { + Id = 1, + BallotId = 1, + CandidateId = 1, + CategoryId = 1 + }, + new + { + Id = 2, + BallotId = 1, + CandidateId = 4, + CategoryId = 2 + }, + new + { + Id = 3, + BallotId = 2, + CandidateId = 2, + CategoryId = 1 + }, + new + { + Id = 4, + BallotId = 2, + CandidateId = 6, + CategoryId = 3 + }); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Results") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.HasOne("Backend.Domain.Category", "Category") + .WithMany("Candidates") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Categories") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId"); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.HasOne("Backend.Domain.VoteBallot", "Ballot") + .WithMany("Entries") + .HasForeignKey("BallotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ballot"); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Navigation("Candidates"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Navigation("Categories"); + + b.Navigation("Results"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Navigation("Entries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/Migrations/20260623131228_AddAwardResultCategoryLock.cs b/Backend/Migrations/20260623131228_AddAwardResultCategoryLock.cs new file mode 100644 index 0000000..07f82dd --- /dev/null +++ b/Backend/Migrations/20260623131228_AddAwardResultCategoryLock.cs @@ -0,0 +1,93 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backend.Migrations +{ + /// + public partial class AddAwardResultCategoryLock : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Results_SeasonId", + table: "Results"); + + migrationBuilder.AddColumn( + name: "CategoryId", + table: "Results", + type: "integer", + nullable: true); + + migrationBuilder.Sql( + """ + UPDATE "Results" AS r + SET "CategoryId" = c."Id" + FROM "Categories" AS c + WHERE r."SeasonId" = c."SeasonId" + AND lower(trim(r."CategoryName")) = lower(trim(c."Name")); + + DO $$ + BEGIN + IF EXISTS (SELECT 1 FROM "Results" WHERE "CategoryId" IS NULL) THEN + RAISE EXCEPTION 'Could not backfill CategoryId for one or more rows in Results.'; + END IF; + END $$; + """); + + migrationBuilder.AlterColumn( + name: "CategoryId", + table: "Results", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Results_CategoryId", + table: "Results", + column: "CategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_Results_SeasonId_CategoryId", + table: "Results", + columns: new[] { "SeasonId", "CategoryId" }, + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_Results_Categories_CategoryId", + table: "Results", + column: "CategoryId", + principalTable: "Categories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Results_Categories_CategoryId", + table: "Results"); + + migrationBuilder.DropIndex( + name: "IX_Results_CategoryId", + table: "Results"); + + migrationBuilder.DropIndex( + name: "IX_Results_SeasonId_CategoryId", + table: "Results"); + + migrationBuilder.DropColumn( + name: "CategoryId", + table: "Results"); + + migrationBuilder.CreateIndex( + name: "IX_Results_SeasonId", + table: "Results", + column: "SeasonId"); + } + } +} diff --git a/Backend/Migrations/20260623152322_AddSeasonShowStreamUrl.Designer.cs b/Backend/Migrations/20260623152322_AddSeasonShowStreamUrl.Designer.cs new file mode 100644 index 0000000..e8fa06c --- /dev/null +++ b/Backend/Migrations/20260623152322_AddSeasonShowStreamUrl.Designer.cs @@ -0,0 +1,1128 @@ +// +using System; +using Backend.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backend.Migrations +{ + [DbContext(typeof(AwardsDbContext))] + [Migration("20260623152322_AddSeasonShowStreamUrl")] + partial class AddSeasonShowStreamUrl + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backend.Domain.AdminAuditEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("AdminTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.HasKey("Id"); + + b.ToTable("AdminAuditEntries"); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CategoryName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "CategoryId") + .IsUnique(); + + b.ToTable("Results"); + + b.HasData( + new + { + Id = 1, + CandidateId = 8, + CategoryId = 5, + CategoryName = "VTuber des Jahres", + SeasonId = 2 + }, + new + { + Id = 2, + CandidateId = 9, + CategoryId = 6, + CategoryName = "Bestes Live Event", + SeasonId = 2 + }, + new + { + Id = 3, + CandidateId = 10, + CategoryId = 7, + CategoryName = "Clip des Jahres", + SeasonId = 2 + }, + new + { + Id = 4, + CandidateId = 11, + CategoryId = 8, + CategoryName = "VTuber des Jahres", + SeasonId = 3 + }, + new + { + Id = 5, + CandidateId = 12, + CategoryId = 9, + CategoryName = "Clip des Jahres", + SeasonId = 3 + }, + new + { + Id = 6, + CandidateId = 13, + CategoryId = 10, + CategoryName = "VTuber des Jahres", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ChannelSlug") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId"); + + b.ToTable("Candidates"); + + b.HasData( + new + { + Id = 1, + CategoryId = 1, + ChannelSlug = "@hoshimimiyu", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 2, + CategoryId = 1, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 3, + CategoryId = 1, + ChannelSlug = "@shiroch", + DisplayName = "Shiro Ch.", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 4, + CategoryId = 2, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 5, + CategoryId = 2, + ChannelSlug = "@aoisakura", + DisplayName = "Aoi Sakura Showcase", + Platform = "YouTube", + SeasonId = 1 + }, + new + { + Id = 6, + CategoryId = 3, + ChannelSlug = "@pyonkichikingdom", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 7, + CategoryId = 4, + ChannelSlug = "@moonrelay", + DisplayName = "Moonrelay", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 8, + CategoryId = 5, + ChannelSlug = "@hoshimimiyu", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 9, + CategoryId = 6, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 10, + CategoryId = 7, + ChannelSlug = "@pyonkichikingdom", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 11, + CategoryId = 8, + ChannelSlug = "@aoisakura", + DisplayName = "Aoi Sakura", + Platform = "YouTube", + SeasonId = 3 + }, + new + { + Id = 12, + CategoryId = 9, + ChannelSlug = "@starbyte", + DisplayName = "Starbyte", + Platform = "Twitch", + SeasonId = 3 + }, + new + { + Id = 13, + CategoryId = 10, + ChannelSlug = "@tenshivox", + DisplayName = "Tenshi Vox", + Platform = "Twitch", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MaxNomineesPerUser") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Slug") + .IsUnique(); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = 1, + Description = "Die groesste Auszeichnung des Jahres.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 1, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 2, + Description = "Events, Konzerte und 3D-Shows.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 1, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 3, + Description = "Der lustigste oder emotionalste Clip des Jahres.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 1, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 4, + Description = "Die aktivste und freundlichste Community.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "Beste Community", + SeasonId = 1, + Slug = "beste-community", + SortOrder = 4 + }, + new + { + Id = 5, + Description = "Archivkategorie 2025.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 2, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 6, + Description = "Archivkategorie 2025.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 2, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 7, + Description = "Archivkategorie 2025.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 2, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 8, + Description = "Archivkategorie 2024.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 3, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 9, + Description = "Archivkategorie 2024.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 3, + Slug = "clip-des-jahres", + SortOrder = 2 + }, + new + { + Id = 10, + Description = "Archivkategorie 2023.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 4, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ClipUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Creator") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("ClipSubmissions"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CandidateText") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("Nominations"); + + b.HasData( + new + { + Id = 1, + CandidateText = "Hoshimi Miyu", + CategoryId = 1, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 13, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_hoshi" + }, + new + { + Id = 2, + CandidateText = "Kurainu 3D Live", + CategoryId = 2, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 14, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_kurainu" + }); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("TwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("RiskFlags"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrentPhase") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("IsCommunityOnly") + .HasColumnType("boolean"); + + b.Property("IsCurrent") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("NominationEndsAt") + .HasColumnType("date"); + + b.Property("NominationStartsAt") + .HasColumnType("date"); + + b.Property("ReviewEndsAt") + .HasColumnType("date"); + + b.Property("ReviewStartsAt") + .HasColumnType("date"); + + b.Property("ShowDate") + .HasColumnType("date"); + + b.Property("ShowStreamUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("VotingEndsAt") + .HasColumnType("date"); + + b.Property("VotingStartsAt") + .HasColumnType("date"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("Seasons"); + + b.HasData( + new + { + Id = 1, + CurrentPhase = "Community Voting", + IsCommunityOnly = true, + IsCurrent = true, + Name = "VTuber Star Awards 2026", + NominationEndsAt = new DateOnly(2026, 5, 31), + NominationStartsAt = new DateOnly(2026, 5, 1), + ReviewEndsAt = new DateOnly(2026, 7, 10), + ReviewStartsAt = new DateOnly(2026, 7, 1), + ShowDate = new DateOnly(2026, 7, 20), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2026, 6, 30), + VotingStartsAt = new DateOnly(2026, 6, 1), + Year = 2026 + }, + new + { + Id = 2, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2025", + NominationEndsAt = new DateOnly(2025, 5, 31), + NominationStartsAt = new DateOnly(2025, 5, 1), + ReviewEndsAt = new DateOnly(2025, 7, 10), + ReviewStartsAt = new DateOnly(2025, 7, 1), + ShowDate = new DateOnly(2025, 7, 20), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2025, 6, 30), + VotingStartsAt = new DateOnly(2025, 6, 1), + Year = 2025 + }, + new + { + Id = 3, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2024", + NominationEndsAt = new DateOnly(2024, 5, 31), + NominationStartsAt = new DateOnly(2024, 5, 1), + ReviewEndsAt = new DateOnly(2024, 7, 10), + ReviewStartsAt = new DateOnly(2024, 7, 1), + ShowDate = new DateOnly(2024, 7, 20), + ShowStreamUrl = "https://youtube.com/c/Jayuhime", + VotingEndsAt = new DateOnly(2024, 6, 30), + VotingStartsAt = new DateOnly(2024, 6, 1), + Year = 2024 + }, + new + { + Id = 4, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2023", + NominationEndsAt = new DateOnly(2023, 5, 31), + NominationStartsAt = new DateOnly(2023, 5, 1), + ReviewEndsAt = new DateOnly(2023, 7, 10), + ReviewStartsAt = new DateOnly(2023, 7, 1), + ShowDate = new DateOnly(2023, 7, 20), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2023, 6, 30), + VotingStartsAt = new DateOnly(2023, 6, 1), + Year = 2023 + }); + }); + + modelBuilder.Entity("Backend.Domain.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SessionToken") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SessionToken") + .IsUnique(); + + b.ToTable("UserSessions"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("VoteBallots"); + + b.HasData( + new + { + Id = 1, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_1" + }, + new + { + Id = 2, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 5, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_2" + }); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BallotId") + .HasColumnType("integer"); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BallotId"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.ToTable("VoteEntries"); + + b.HasData( + new + { + Id = 1, + BallotId = 1, + CandidateId = 1, + CategoryId = 1 + }, + new + { + Id = 2, + BallotId = 1, + CandidateId = 4, + CategoryId = 2 + }, + new + { + Id = 3, + BallotId = 2, + CandidateId = 2, + CategoryId = 1 + }, + new + { + Id = 4, + BallotId = 2, + CandidateId = 6, + CategoryId = 3 + }); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Results") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.HasOne("Backend.Domain.Category", "Category") + .WithMany("Candidates") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Categories") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId"); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.HasOne("Backend.Domain.VoteBallot", "Ballot") + .WithMany("Entries") + .HasForeignKey("BallotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ballot"); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Navigation("Candidates"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Navigation("Categories"); + + b.Navigation("Results"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Navigation("Entries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/Migrations/20260623152322_AddSeasonShowStreamUrl.cs b/Backend/Migrations/20260623152322_AddSeasonShowStreamUrl.cs new file mode 100644 index 0000000..06fef06 --- /dev/null +++ b/Backend/Migrations/20260623152322_AddSeasonShowStreamUrl.cs @@ -0,0 +1,58 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backend.Migrations +{ + /// + public partial class AddSeasonShowStreamUrl : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ShowStreamUrl", + table: "Seasons", + type: "character varying(400)", + maxLength: 400, + nullable: false, + defaultValue: ""); + + migrationBuilder.UpdateData( + table: "Seasons", + keyColumn: "Id", + keyValue: 1, + column: "ShowStreamUrl", + value: "https://twitch.tv/jayuhime"); + + migrationBuilder.UpdateData( + table: "Seasons", + keyColumn: "Id", + keyValue: 2, + column: "ShowStreamUrl", + value: "https://twitch.tv/jayuhime"); + + migrationBuilder.UpdateData( + table: "Seasons", + keyColumn: "Id", + keyValue: 3, + column: "ShowStreamUrl", + value: "https://youtube.com/c/Jayuhime"); + + migrationBuilder.UpdateData( + table: "Seasons", + keyColumn: "Id", + keyValue: 4, + column: "ShowStreamUrl", + value: "https://twitch.tv/jayuhime"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ShowStreamUrl", + table: "Seasons"); + } + } +} diff --git a/Backend/Migrations/20260623153212_AddSiteSettings.Designer.cs b/Backend/Migrations/20260623153212_AddSiteSettings.Designer.cs new file mode 100644 index 0000000..ff76a51 --- /dev/null +++ b/Backend/Migrations/20260623153212_AddSiteSettings.Designer.cs @@ -0,0 +1,1199 @@ +// +using System; +using Backend.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backend.Migrations +{ + [DbContext(typeof(AwardsDbContext))] + [Migration("20260623153212_AddSiteSettings")] + partial class AddSiteSettings + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backend.Domain.AdminAuditEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("AdminTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.HasKey("Id"); + + b.ToTable("AdminAuditEntries"); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CategoryName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "CategoryId") + .IsUnique(); + + b.ToTable("Results"); + + b.HasData( + new + { + Id = 1, + CandidateId = 8, + CategoryId = 5, + CategoryName = "VTuber des Jahres", + SeasonId = 2 + }, + new + { + Id = 2, + CandidateId = 9, + CategoryId = 6, + CategoryName = "Bestes Live Event", + SeasonId = 2 + }, + new + { + Id = 3, + CandidateId = 10, + CategoryId = 7, + CategoryName = "Clip des Jahres", + SeasonId = 2 + }, + new + { + Id = 4, + CandidateId = 11, + CategoryId = 8, + CategoryName = "VTuber des Jahres", + SeasonId = 3 + }, + new + { + Id = 5, + CandidateId = 12, + CategoryId = 9, + CategoryName = "Clip des Jahres", + SeasonId = 3 + }, + new + { + Id = 6, + CandidateId = 13, + CategoryId = 10, + CategoryName = "VTuber des Jahres", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ChannelSlug") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId"); + + b.ToTable("Candidates"); + + b.HasData( + new + { + Id = 1, + CategoryId = 1, + ChannelSlug = "@hoshimimiyu", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 2, + CategoryId = 1, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 3, + CategoryId = 1, + ChannelSlug = "@shiroch", + DisplayName = "Shiro Ch.", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 4, + CategoryId = 2, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 5, + CategoryId = 2, + ChannelSlug = "@aoisakura", + DisplayName = "Aoi Sakura Showcase", + Platform = "YouTube", + SeasonId = 1 + }, + new + { + Id = 6, + CategoryId = 3, + ChannelSlug = "@pyonkichikingdom", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 7, + CategoryId = 4, + ChannelSlug = "@moonrelay", + DisplayName = "Moonrelay", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 8, + CategoryId = 5, + ChannelSlug = "@hoshimimiyu", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 9, + CategoryId = 6, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 10, + CategoryId = 7, + ChannelSlug = "@pyonkichikingdom", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 11, + CategoryId = 8, + ChannelSlug = "@aoisakura", + DisplayName = "Aoi Sakura", + Platform = "YouTube", + SeasonId = 3 + }, + new + { + Id = 12, + CategoryId = 9, + ChannelSlug = "@starbyte", + DisplayName = "Starbyte", + Platform = "Twitch", + SeasonId = 3 + }, + new + { + Id = 13, + CategoryId = 10, + ChannelSlug = "@tenshivox", + DisplayName = "Tenshi Vox", + Platform = "Twitch", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MaxNomineesPerUser") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Slug") + .IsUnique(); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = 1, + Description = "Die groesste Auszeichnung des Jahres.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 1, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 2, + Description = "Events, Konzerte und 3D-Shows.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 1, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 3, + Description = "Der lustigste oder emotionalste Clip des Jahres.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 1, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 4, + Description = "Die aktivste und freundlichste Community.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "Beste Community", + SeasonId = 1, + Slug = "beste-community", + SortOrder = 4 + }, + new + { + Id = 5, + Description = "Archivkategorie 2025.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 2, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 6, + Description = "Archivkategorie 2025.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 2, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 7, + Description = "Archivkategorie 2025.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 2, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 8, + Description = "Archivkategorie 2024.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 3, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 9, + Description = "Archivkategorie 2024.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 3, + Slug = "clip-des-jahres", + SortOrder = 2 + }, + new + { + Id = 10, + Description = "Archivkategorie 2023.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 4, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ClipUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Creator") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("ClipSubmissions"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CandidateText") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("Nominations"); + + b.HasData( + new + { + Id = 1, + CandidateText = "Hoshimi Miyu", + CategoryId = 1, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 13, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_hoshi" + }, + new + { + Id = 2, + CandidateText = "Kurainu 3D Live", + CategoryId = 2, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 14, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_kurainu" + }); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("TwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("RiskFlags"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrentPhase") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("IsCommunityOnly") + .HasColumnType("boolean"); + + b.Property("IsCurrent") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("NominationEndsAt") + .HasColumnType("date"); + + b.Property("NominationStartsAt") + .HasColumnType("date"); + + b.Property("ReviewEndsAt") + .HasColumnType("date"); + + b.Property("ReviewStartsAt") + .HasColumnType("date"); + + b.Property("ShowDate") + .HasColumnType("date"); + + b.Property("ShowStreamUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("VotingEndsAt") + .HasColumnType("date"); + + b.Property("VotingStartsAt") + .HasColumnType("date"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("Seasons"); + + b.HasData( + new + { + Id = 1, + CurrentPhase = "Community Voting", + IsCommunityOnly = true, + IsCurrent = true, + Name = "VTuber Star Awards 2026", + NominationEndsAt = new DateOnly(2026, 5, 31), + NominationStartsAt = new DateOnly(2026, 5, 1), + ReviewEndsAt = new DateOnly(2026, 7, 10), + ReviewStartsAt = new DateOnly(2026, 7, 1), + ShowDate = new DateOnly(2026, 7, 20), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2026, 6, 30), + VotingStartsAt = new DateOnly(2026, 6, 1), + Year = 2026 + }, + new + { + Id = 2, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2025", + NominationEndsAt = new DateOnly(2025, 5, 31), + NominationStartsAt = new DateOnly(2025, 5, 1), + ReviewEndsAt = new DateOnly(2025, 7, 10), + ReviewStartsAt = new DateOnly(2025, 7, 1), + ShowDate = new DateOnly(2025, 7, 20), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2025, 6, 30), + VotingStartsAt = new DateOnly(2025, 6, 1), + Year = 2025 + }, + new + { + Id = 3, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2024", + NominationEndsAt = new DateOnly(2024, 5, 31), + NominationStartsAt = new DateOnly(2024, 5, 1), + ReviewEndsAt = new DateOnly(2024, 7, 10), + ReviewStartsAt = new DateOnly(2024, 7, 1), + ShowDate = new DateOnly(2024, 7, 20), + ShowStreamUrl = "https://youtube.com/c/Jayuhime", + VotingEndsAt = new DateOnly(2024, 6, 30), + VotingStartsAt = new DateOnly(2024, 6, 1), + Year = 2024 + }, + new + { + Id = 4, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2023", + NominationEndsAt = new DateOnly(2023, 5, 31), + NominationStartsAt = new DateOnly(2023, 5, 1), + ReviewEndsAt = new DateOnly(2023, 7, 10), + ReviewStartsAt = new DateOnly(2023, 7, 1), + ShowDate = new DateOnly(2023, 7, 20), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2023, 6, 30), + VotingStartsAt = new DateOnly(2023, 6, 1), + Year = 2023 + }); + }); + + modelBuilder.Entity("Backend.Domain.SiteSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContactUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("FaqJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("HostDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("HostTagline") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("ImprintUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("NewsletterUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("PrivacyEmail") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("SocialLinksJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.ToTable("SiteSettings"); + + b.HasData( + new + { + Id = 1, + ContactUrl = "https://vtuber-star-awards.de/kontakt", + FaqJson = "[{\"question\":\"Wer darf mitmachen?\",\"answer\":\"Jede:r mit einem Twitch-Account. Einmal einloggen gen\\u00FCgt \\u2013 kein extra Konto, kein Papierkram.\"},{\"question\":\"Wie werden die Gewinner bestimmt?\",\"answer\":\"Komplett durch eure Stimmen. Die Community entscheidet, wer auf die B\\u00FChne darf.\"},{\"question\":\"Kann ich meine Wahl noch \\u00E4ndern?\",\"answer\":\"Ja. Bis zum Ende der Voting-Phase kannst du Nominierungen und Stimmen anpassen.\"},{\"question\":\"Wann und wo findet die Show statt?\",\"answer\":\"Die gro\\u00DFe Live-Show wird von Jayuhime gehostet und live auf den verkn\\u00FCpften Plattformen \\u00FCbertragen.\"}]", + HostDisplayName = "Jayuhime", + HostTagline = "VTuber & Award Host", + ImprintUrl = "https://vtuber-star-awards.de/impressum", + NewsletterUrl = "https://vtuber-star-awards.de/newsletter", + PrivacyEmail = "datenschutz@vtuber-star-awards.de", + SocialLinksJson = "[{\"label\":\"Twitch\",\"platform\":\"twitch\",\"url\":\"https://twitch.tv/jayuhime\"},{\"label\":\"YouTube\",\"platform\":\"youtube\",\"url\":\"https://youtube.com/c/Jayuhime\"},{\"label\":\"X\",\"platform\":\"x\",\"url\":\"https://x.com/jayuhime\"},{\"label\":\"Instagram\",\"platform\":\"instagram\",\"url\":\"https://instagram.com/jayuhime\"},{\"label\":\"Discord\",\"platform\":\"discord\",\"url\":\"https://discord.gg/jayuhime\"}]", + SponsorsUrl = "https://vtuber-star-awards.de/partner" + }); + }); + + modelBuilder.Entity("Backend.Domain.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SessionToken") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SessionToken") + .IsUnique(); + + b.ToTable("UserSessions"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("VoteBallots"); + + b.HasData( + new + { + Id = 1, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_1" + }, + new + { + Id = 2, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 5, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_2" + }); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BallotId") + .HasColumnType("integer"); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BallotId"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.ToTable("VoteEntries"); + + b.HasData( + new + { + Id = 1, + BallotId = 1, + CandidateId = 1, + CategoryId = 1 + }, + new + { + Id = 2, + BallotId = 1, + CandidateId = 4, + CategoryId = 2 + }, + new + { + Id = 3, + BallotId = 2, + CandidateId = 2, + CategoryId = 1 + }, + new + { + Id = 4, + BallotId = 2, + CandidateId = 6, + CategoryId = 3 + }); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Results") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.HasOne("Backend.Domain.Category", "Category") + .WithMany("Candidates") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Categories") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId"); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.HasOne("Backend.Domain.VoteBallot", "Ballot") + .WithMany("Entries") + .HasForeignKey("BallotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ballot"); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Navigation("Candidates"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Navigation("Categories"); + + b.Navigation("Results"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Navigation("Entries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/Migrations/20260623153212_AddSiteSettings.cs b/Backend/Migrations/20260623153212_AddSiteSettings.cs new file mode 100644 index 0000000..2840759 --- /dev/null +++ b/Backend/Migrations/20260623153212_AddSiteSettings.cs @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backend.Migrations +{ + /// + public partial class AddSiteSettings : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "SiteSettings", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + HostDisplayName = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), + HostTagline = table.Column(type: "character varying(160)", maxLength: 160, nullable: false), + NewsletterUrl = table.Column(type: "character varying(400)", maxLength: 400, nullable: false), + PrivacyEmail = table.Column(type: "character varying(160)", maxLength: 160, nullable: false), + ImprintUrl = table.Column(type: "character varying(400)", maxLength: 400, nullable: false), + ContactUrl = table.Column(type: "character varying(400)", maxLength: 400, nullable: false), + SponsorsUrl = table.Column(type: "character varying(400)", maxLength: 400, nullable: false), + SocialLinksJson = table.Column(type: "text", nullable: false), + FaqJson = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SiteSettings", x => x.Id); + }); + + migrationBuilder.InsertData( + table: "SiteSettings", + columns: new[] { "Id", "ContactUrl", "FaqJson", "HostDisplayName", "HostTagline", "ImprintUrl", "NewsletterUrl", "PrivacyEmail", "SocialLinksJson", "SponsorsUrl" }, + values: new object[] { 1, "https://vtuber-star-awards.de/kontakt", "[{\"question\":\"Wer darf mitmachen?\",\"answer\":\"Jede:r mit einem Twitch-Account. Einmal einloggen gen\\u00FCgt \\u2013 kein extra Konto, kein Papierkram.\"},{\"question\":\"Wie werden die Gewinner bestimmt?\",\"answer\":\"Komplett durch eure Stimmen. Die Community entscheidet, wer auf die B\\u00FChne darf.\"},{\"question\":\"Kann ich meine Wahl noch \\u00E4ndern?\",\"answer\":\"Ja. Bis zum Ende der Voting-Phase kannst du Nominierungen und Stimmen anpassen.\"},{\"question\":\"Wann und wo findet die Show statt?\",\"answer\":\"Die gro\\u00DFe Live-Show wird von Jayuhime gehostet und live auf den verkn\\u00FCpften Plattformen \\u00FCbertragen.\"}]", "Jayuhime", "VTuber & Award Host", "https://vtuber-star-awards.de/impressum", "https://vtuber-star-awards.de/newsletter", "datenschutz@vtuber-star-awards.de", "[{\"label\":\"Twitch\",\"platform\":\"twitch\",\"url\":\"https://twitch.tv/jayuhime\"},{\"label\":\"YouTube\",\"platform\":\"youtube\",\"url\":\"https://youtube.com/c/Jayuhime\"},{\"label\":\"X\",\"platform\":\"x\",\"url\":\"https://x.com/jayuhime\"},{\"label\":\"Instagram\",\"platform\":\"instagram\",\"url\":\"https://instagram.com/jayuhime\"},{\"label\":\"Discord\",\"platform\":\"discord\",\"url\":\"https://discord.gg/jayuhime\"}]", "https://vtuber-star-awards.de/partner" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "SiteSettings"); + } + } +} diff --git a/Backend/Migrations/20260623154438_AddPrivacyPolicyContentMetadata.Designer.cs b/Backend/Migrations/20260623154438_AddPrivacyPolicyContentMetadata.Designer.cs new file mode 100644 index 0000000..cd5b793 --- /dev/null +++ b/Backend/Migrations/20260623154438_AddPrivacyPolicyContentMetadata.Designer.cs @@ -0,0 +1,1213 @@ +// +using System; +using Backend.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backend.Migrations +{ + [DbContext(typeof(AwardsDbContext))] + [Migration("20260623154438_AddPrivacyPolicyContentMetadata")] + partial class AddPrivacyPolicyContentMetadata + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backend.Domain.AdminAuditEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("AdminTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.HasKey("Id"); + + b.ToTable("AdminAuditEntries"); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CategoryName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "CategoryId") + .IsUnique(); + + b.ToTable("Results"); + + b.HasData( + new + { + Id = 1, + CandidateId = 8, + CategoryId = 5, + CategoryName = "VTuber des Jahres", + SeasonId = 2 + }, + new + { + Id = 2, + CandidateId = 9, + CategoryId = 6, + CategoryName = "Bestes Live Event", + SeasonId = 2 + }, + new + { + Id = 3, + CandidateId = 10, + CategoryId = 7, + CategoryName = "Clip des Jahres", + SeasonId = 2 + }, + new + { + Id = 4, + CandidateId = 11, + CategoryId = 8, + CategoryName = "VTuber des Jahres", + SeasonId = 3 + }, + new + { + Id = 5, + CandidateId = 12, + CategoryId = 9, + CategoryName = "Clip des Jahres", + SeasonId = 3 + }, + new + { + Id = 6, + CandidateId = 13, + CategoryId = 10, + CategoryName = "VTuber des Jahres", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ChannelSlug") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId"); + + b.ToTable("Candidates"); + + b.HasData( + new + { + Id = 1, + CategoryId = 1, + ChannelSlug = "@hoshimimiyu", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 2, + CategoryId = 1, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 3, + CategoryId = 1, + ChannelSlug = "@shiroch", + DisplayName = "Shiro Ch.", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 4, + CategoryId = 2, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 5, + CategoryId = 2, + ChannelSlug = "@aoisakura", + DisplayName = "Aoi Sakura Showcase", + Platform = "YouTube", + SeasonId = 1 + }, + new + { + Id = 6, + CategoryId = 3, + ChannelSlug = "@pyonkichikingdom", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 7, + CategoryId = 4, + ChannelSlug = "@moonrelay", + DisplayName = "Moonrelay", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 8, + CategoryId = 5, + ChannelSlug = "@hoshimimiyu", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 9, + CategoryId = 6, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 10, + CategoryId = 7, + ChannelSlug = "@pyonkichikingdom", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 11, + CategoryId = 8, + ChannelSlug = "@aoisakura", + DisplayName = "Aoi Sakura", + Platform = "YouTube", + SeasonId = 3 + }, + new + { + Id = 12, + CategoryId = 9, + ChannelSlug = "@starbyte", + DisplayName = "Starbyte", + Platform = "Twitch", + SeasonId = 3 + }, + new + { + Id = 13, + CategoryId = 10, + ChannelSlug = "@tenshivox", + DisplayName = "Tenshi Vox", + Platform = "Twitch", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MaxNomineesPerUser") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Slug") + .IsUnique(); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = 1, + Description = "Die groesste Auszeichnung des Jahres.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 1, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 2, + Description = "Events, Konzerte und 3D-Shows.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 1, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 3, + Description = "Der lustigste oder emotionalste Clip des Jahres.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 1, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 4, + Description = "Die aktivste und freundlichste Community.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "Beste Community", + SeasonId = 1, + Slug = "beste-community", + SortOrder = 4 + }, + new + { + Id = 5, + Description = "Archivkategorie 2025.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 2, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 6, + Description = "Archivkategorie 2025.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 2, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 7, + Description = "Archivkategorie 2025.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 2, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 8, + Description = "Archivkategorie 2024.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 3, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 9, + Description = "Archivkategorie 2024.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 3, + Slug = "clip-des-jahres", + SortOrder = 2 + }, + new + { + Id = 10, + Description = "Archivkategorie 2023.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 4, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ClipUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Creator") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("ClipSubmissions"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CandidateText") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("Nominations"); + + b.HasData( + new + { + Id = 1, + CandidateText = "Hoshimi Miyu", + CategoryId = 1, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 13, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_hoshi" + }, + new + { + Id = 2, + CandidateText = "Kurainu 3D Live", + CategoryId = 2, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 14, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_kurainu" + }); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("TwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("RiskFlags"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrentPhase") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("IsCommunityOnly") + .HasColumnType("boolean"); + + b.Property("IsCurrent") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("NominationEndsAt") + .HasColumnType("date"); + + b.Property("NominationStartsAt") + .HasColumnType("date"); + + b.Property("ReviewEndsAt") + .HasColumnType("date"); + + b.Property("ReviewStartsAt") + .HasColumnType("date"); + + b.Property("ShowDate") + .HasColumnType("date"); + + b.Property("ShowStreamUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("VotingEndsAt") + .HasColumnType("date"); + + b.Property("VotingStartsAt") + .HasColumnType("date"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("Seasons"); + + b.HasData( + new + { + Id = 1, + CurrentPhase = "Community Voting", + IsCommunityOnly = true, + IsCurrent = true, + Name = "VTuber Star Awards 2026", + NominationEndsAt = new DateOnly(2026, 5, 31), + NominationStartsAt = new DateOnly(2026, 5, 1), + ReviewEndsAt = new DateOnly(2026, 7, 10), + ReviewStartsAt = new DateOnly(2026, 7, 1), + ShowDate = new DateOnly(2026, 7, 20), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2026, 6, 30), + VotingStartsAt = new DateOnly(2026, 6, 1), + Year = 2026 + }, + new + { + Id = 2, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2025", + NominationEndsAt = new DateOnly(2025, 5, 31), + NominationStartsAt = new DateOnly(2025, 5, 1), + ReviewEndsAt = new DateOnly(2025, 7, 10), + ReviewStartsAt = new DateOnly(2025, 7, 1), + ShowDate = new DateOnly(2025, 7, 20), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2025, 6, 30), + VotingStartsAt = new DateOnly(2025, 6, 1), + Year = 2025 + }, + new + { + Id = 3, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2024", + NominationEndsAt = new DateOnly(2024, 5, 31), + NominationStartsAt = new DateOnly(2024, 5, 1), + ReviewEndsAt = new DateOnly(2024, 7, 10), + ReviewStartsAt = new DateOnly(2024, 7, 1), + ShowDate = new DateOnly(2024, 7, 20), + ShowStreamUrl = "https://youtube.com/c/Jayuhime", + VotingEndsAt = new DateOnly(2024, 6, 30), + VotingStartsAt = new DateOnly(2024, 6, 1), + Year = 2024 + }, + new + { + Id = 4, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2023", + NominationEndsAt = new DateOnly(2023, 5, 31), + NominationStartsAt = new DateOnly(2023, 5, 1), + ReviewEndsAt = new DateOnly(2023, 7, 10), + ReviewStartsAt = new DateOnly(2023, 7, 1), + ShowDate = new DateOnly(2023, 7, 20), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2023, 6, 30), + VotingStartsAt = new DateOnly(2023, 6, 1), + Year = 2023 + }); + }); + + modelBuilder.Entity("Backend.Domain.SiteSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContactUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("FaqJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("HostDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("HostTagline") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("ImprintUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("NewsletterUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("PrivacyEmail") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("PrivacyPolicyContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("PrivacyPolicyUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivacyPolicyUpdatedBy") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SocialLinksJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.ToTable("SiteSettings"); + + b.HasData( + new + { + Id = 1, + ContactUrl = "https://vtuber-star-awards.de/kontakt", + FaqJson = "[{\"question\":\"Wer darf mitmachen?\",\"answer\":\"Jede:r mit einem Twitch-Account. Einmal einloggen gen\\u00FCgt \\u2013 kein extra Konto, kein Papierkram.\"},{\"question\":\"Wie werden die Gewinner bestimmt?\",\"answer\":\"Komplett durch eure Stimmen. Die Community entscheidet, wer auf die B\\u00FChne darf.\"},{\"question\":\"Kann ich meine Wahl noch \\u00E4ndern?\",\"answer\":\"Ja. Bis zum Ende der Voting-Phase kannst du Nominierungen und Stimmen anpassen.\"},{\"question\":\"Wann und wo findet die Show statt?\",\"answer\":\"Die gro\\u00DFe Live-Show wird von Jayuhime gehostet und live auf den verkn\\u00FCpften Plattformen \\u00FCbertragen.\"}]", + HostDisplayName = "Jayuhime", + HostTagline = "VTuber & Award Host", + ImprintUrl = "https://vtuber-star-awards.de/impressum", + NewsletterUrl = "https://vtuber-star-awards.de/newsletter", + PrivacyEmail = "datenschutz@vtuber-star-awards.de", + PrivacyPolicyContent = "Verantwortliche:r\nVTuber Star Awards, vertreten durch Jayuhime. Kontakt: datenschutz@vtuber-star-awards.de\n\nWelche Daten wir verarbeiten\nBei Teilnahme (Voting, Nominierung, Clip-Einreichung) verarbeiten wir ausschließlich deine Twitch-User-ID sowie den Zeitstempel deiner Aktion.\nFür Show-Erinnerungen speichern wir optional deine E-Mail-Adresse.\n\nRechtsgrundlage\nVerarbeitung auf Basis von Art. 6 Abs. 1 lit. b DSGVO. Für optionale Erinnerungen gilt Art. 6 Abs. 1 lit. a DSGVO.\n\nZweck der Verarbeitung\nDurchführung des VTuber Star Awards, Sicherstellung fairer Abstimmung, Spam-Prävention sowie Admin-Review von Einreichungen.\n\nLöschfristen\nAlle Teilnahmedaten werden spätestens 6 Monate nach der Award-Show automatisch gelöscht. E-Mail-Adressen werden nach der Show entfernt.\n\nDeine Rechte\nDu hast das Recht auf Auskunft, Löschung, Berichtigung, Widerspruch und Beschwerde bei der zuständigen Aufsichtsbehörde.\n\nWeitergabe an Dritte\nKeine Weitergabe zu Werbezwecken. Technische Dienstleister verarbeiten Daten ausschließlich im Rahmen der Auftragsverarbeitung.", + PrivacyPolicyUpdatedAt = new DateTimeOffset(new DateTime(2026, 6, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + PrivacyPolicyUpdatedBy = "seed", + SocialLinksJson = "[{\"label\":\"Twitch\",\"platform\":\"twitch\",\"url\":\"https://twitch.tv/jayuhime\"},{\"label\":\"YouTube\",\"platform\":\"youtube\",\"url\":\"https://youtube.com/c/Jayuhime\"},{\"label\":\"X\",\"platform\":\"x\",\"url\":\"https://x.com/jayuhime\"},{\"label\":\"Instagram\",\"platform\":\"instagram\",\"url\":\"https://instagram.com/jayuhime\"},{\"label\":\"Discord\",\"platform\":\"discord\",\"url\":\"https://discord.gg/jayuhime\"}]", + SponsorsUrl = "https://vtuber-star-awards.de/partner" + }); + }); + + modelBuilder.Entity("Backend.Domain.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SessionToken") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SessionToken") + .IsUnique(); + + b.ToTable("UserSessions"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("VoteBallots"); + + b.HasData( + new + { + Id = 1, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_1" + }, + new + { + Id = 2, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 5, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_2" + }); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BallotId") + .HasColumnType("integer"); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BallotId"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.ToTable("VoteEntries"); + + b.HasData( + new + { + Id = 1, + BallotId = 1, + CandidateId = 1, + CategoryId = 1 + }, + new + { + Id = 2, + BallotId = 1, + CandidateId = 4, + CategoryId = 2 + }, + new + { + Id = 3, + BallotId = 2, + CandidateId = 2, + CategoryId = 1 + }, + new + { + Id = 4, + BallotId = 2, + CandidateId = 6, + CategoryId = 3 + }); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Results") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.HasOne("Backend.Domain.Category", "Category") + .WithMany("Candidates") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Categories") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId"); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.HasOne("Backend.Domain.VoteBallot", "Ballot") + .WithMany("Entries") + .HasForeignKey("BallotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ballot"); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Navigation("Candidates"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Navigation("Categories"); + + b.Navigation("Results"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Navigation("Entries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/Migrations/20260623154438_AddPrivacyPolicyContentMetadata.cs b/Backend/Migrations/20260623154438_AddPrivacyPolicyContentMetadata.cs new file mode 100644 index 0000000..de3d9e0 --- /dev/null +++ b/Backend/Migrations/20260623154438_AddPrivacyPolicyContentMetadata.cs @@ -0,0 +1,58 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backend.Migrations +{ + /// + public partial class AddPrivacyPolicyContentMetadata : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PrivacyPolicyContent", + table: "SiteSettings", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "PrivacyPolicyUpdatedAt", + table: "SiteSettings", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "PrivacyPolicyUpdatedBy", + table: "SiteSettings", + type: "character varying(120)", + maxLength: 120, + nullable: true); + + migrationBuilder.UpdateData( + table: "SiteSettings", + keyColumn: "Id", + keyValue: 1, + columns: new[] { "PrivacyPolicyContent", "PrivacyPolicyUpdatedAt", "PrivacyPolicyUpdatedBy" }, + values: new object[] { "Verantwortliche:r\nVTuber Star Awards, vertreten durch Jayuhime. Kontakt: datenschutz@vtuber-star-awards.de\n\nWelche Daten wir verarbeiten\nBei Teilnahme (Voting, Nominierung, Clip-Einreichung) verarbeiten wir ausschließlich deine Twitch-User-ID sowie den Zeitstempel deiner Aktion.\nFür Show-Erinnerungen speichern wir optional deine E-Mail-Adresse.\n\nRechtsgrundlage\nVerarbeitung auf Basis von Art. 6 Abs. 1 lit. b DSGVO. Für optionale Erinnerungen gilt Art. 6 Abs. 1 lit. a DSGVO.\n\nZweck der Verarbeitung\nDurchführung des VTuber Star Awards, Sicherstellung fairer Abstimmung, Spam-Prävention sowie Admin-Review von Einreichungen.\n\nLöschfristen\nAlle Teilnahmedaten werden spätestens 6 Monate nach der Award-Show automatisch gelöscht. E-Mail-Adressen werden nach der Show entfernt.\n\nDeine Rechte\nDu hast das Recht auf Auskunft, Löschung, Berichtigung, Widerspruch und Beschwerde bei der zuständigen Aufsichtsbehörde.\n\nWeitergabe an Dritte\nKeine Weitergabe zu Werbezwecken. Technische Dienstleister verarbeiten Daten ausschließlich im Rahmen der Auftragsverarbeitung.", new DateTimeOffset(new DateTime(2026, 6, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "seed" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "PrivacyPolicyContent", + table: "SiteSettings"); + + migrationBuilder.DropColumn( + name: "PrivacyPolicyUpdatedAt", + table: "SiteSettings"); + + migrationBuilder.DropColumn( + name: "PrivacyPolicyUpdatedBy", + table: "SiteSettings"); + } + } +} diff --git a/Backend/Migrations/20260624065545_AddOperationalSiteSettings.Designer.cs b/Backend/Migrations/20260624065545_AddOperationalSiteSettings.Designer.cs new file mode 100644 index 0000000..b5de68c --- /dev/null +++ b/Backend/Migrations/20260624065545_AddOperationalSiteSettings.Designer.cs @@ -0,0 +1,1267 @@ +// +using System; +using Backend.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backend.Migrations +{ + [DbContext(typeof(AwardsDbContext))] + [Migration("20260624065545_AddOperationalSiteSettings")] + partial class AddOperationalSiteSettings + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backend.Domain.AdminAuditEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("AdminTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.HasKey("Id"); + + b.ToTable("AdminAuditEntries"); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CategoryName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "CategoryId") + .IsUnique(); + + b.ToTable("Results"); + + b.HasData( + new + { + Id = 1, + CandidateId = 8, + CategoryId = 5, + CategoryName = "VTuber des Jahres", + SeasonId = 2 + }, + new + { + Id = 2, + CandidateId = 9, + CategoryId = 6, + CategoryName = "Bestes Live Event", + SeasonId = 2 + }, + new + { + Id = 3, + CandidateId = 10, + CategoryId = 7, + CategoryName = "Clip des Jahres", + SeasonId = 2 + }, + new + { + Id = 4, + CandidateId = 11, + CategoryId = 8, + CategoryName = "VTuber des Jahres", + SeasonId = 3 + }, + new + { + Id = 5, + CandidateId = 12, + CategoryId = 9, + CategoryName = "Clip des Jahres", + SeasonId = 3 + }, + new + { + Id = 6, + CandidateId = 13, + CategoryId = 10, + CategoryName = "VTuber des Jahres", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ChannelSlug") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId"); + + b.ToTable("Candidates"); + + b.HasData( + new + { + Id = 1, + CategoryId = 1, + ChannelSlug = "@hoshimimiyu", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 2, + CategoryId = 1, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 3, + CategoryId = 1, + ChannelSlug = "@shiroch", + DisplayName = "Shiro Ch.", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 4, + CategoryId = 2, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 5, + CategoryId = 2, + ChannelSlug = "@aoisakura", + DisplayName = "Aoi Sakura Showcase", + Platform = "YouTube", + SeasonId = 1 + }, + new + { + Id = 6, + CategoryId = 3, + ChannelSlug = "@pyonkichikingdom", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 7, + CategoryId = 4, + ChannelSlug = "@moonrelay", + DisplayName = "Moonrelay", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 8, + CategoryId = 5, + ChannelSlug = "@hoshimimiyu", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 9, + CategoryId = 6, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 10, + CategoryId = 7, + ChannelSlug = "@pyonkichikingdom", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 11, + CategoryId = 8, + ChannelSlug = "@aoisakura", + DisplayName = "Aoi Sakura", + Platform = "YouTube", + SeasonId = 3 + }, + new + { + Id = 12, + CategoryId = 9, + ChannelSlug = "@starbyte", + DisplayName = "Starbyte", + Platform = "Twitch", + SeasonId = 3 + }, + new + { + Id = 13, + CategoryId = 10, + ChannelSlug = "@tenshivox", + DisplayName = "Tenshi Vox", + Platform = "Twitch", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MaxNomineesPerUser") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Slug") + .IsUnique(); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = 1, + Description = "Die groesste Auszeichnung des Jahres.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 1, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 2, + Description = "Events, Konzerte und 3D-Shows.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 1, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 3, + Description = "Der lustigste oder emotionalste Clip des Jahres.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 1, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 4, + Description = "Die aktivste und freundlichste Community.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "Beste Community", + SeasonId = 1, + Slug = "beste-community", + SortOrder = 4 + }, + new + { + Id = 5, + Description = "Archivkategorie 2025.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 2, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 6, + Description = "Archivkategorie 2025.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 2, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 7, + Description = "Archivkategorie 2025.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 2, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 8, + Description = "Archivkategorie 2024.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 3, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 9, + Description = "Archivkategorie 2024.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 3, + Slug = "clip-des-jahres", + SortOrder = 2 + }, + new + { + Id = 10, + Description = "Archivkategorie 2023.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 4, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ClipUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Creator") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("ClipSubmissions"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CandidateText") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("Nominations"); + + b.HasData( + new + { + Id = 1, + CandidateText = "Hoshimi Miyu", + CategoryId = 1, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 13, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_hoshi" + }, + new + { + Id = 2, + CandidateText = "Kurainu 3D Live", + CategoryId = 2, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 14, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_kurainu" + }); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("TwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("RiskFlags"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrentPhase") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("IsCommunityOnly") + .HasColumnType("boolean"); + + b.Property("IsCurrent") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("NominationEndsAt") + .HasColumnType("date"); + + b.Property("NominationStartsAt") + .HasColumnType("date"); + + b.Property("ReviewEndsAt") + .HasColumnType("date"); + + b.Property("ReviewStartsAt") + .HasColumnType("date"); + + b.Property("ShowDate") + .HasColumnType("date"); + + b.Property("ShowStreamUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("VotingEndsAt") + .HasColumnType("date"); + + b.Property("VotingStartsAt") + .HasColumnType("date"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("Seasons"); + + b.HasData( + new + { + Id = 1, + CurrentPhase = "Community Voting", + IsCommunityOnly = true, + IsCurrent = true, + Name = "VTuber Star Awards 2026", + NominationEndsAt = new DateOnly(2026, 5, 31), + NominationStartsAt = new DateOnly(2026, 5, 1), + ReviewEndsAt = new DateOnly(2026, 7, 10), + ReviewStartsAt = new DateOnly(2026, 7, 1), + ShowDate = new DateOnly(2026, 7, 20), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2026, 6, 30), + VotingStartsAt = new DateOnly(2026, 6, 1), + Year = 2026 + }, + new + { + Id = 2, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2025", + NominationEndsAt = new DateOnly(2025, 5, 31), + NominationStartsAt = new DateOnly(2025, 5, 1), + ReviewEndsAt = new DateOnly(2025, 7, 10), + ReviewStartsAt = new DateOnly(2025, 7, 1), + ShowDate = new DateOnly(2025, 7, 20), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2025, 6, 30), + VotingStartsAt = new DateOnly(2025, 6, 1), + Year = 2025 + }, + new + { + Id = 3, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2024", + NominationEndsAt = new DateOnly(2024, 5, 31), + NominationStartsAt = new DateOnly(2024, 5, 1), + ReviewEndsAt = new DateOnly(2024, 7, 10), + ReviewStartsAt = new DateOnly(2024, 7, 1), + ShowDate = new DateOnly(2024, 7, 20), + ShowStreamUrl = "https://youtube.com/c/Jayuhime", + VotingEndsAt = new DateOnly(2024, 6, 30), + VotingStartsAt = new DateOnly(2024, 6, 1), + Year = 2024 + }, + new + { + Id = 4, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2023", + NominationEndsAt = new DateOnly(2023, 5, 31), + NominationStartsAt = new DateOnly(2023, 5, 1), + ReviewEndsAt = new DateOnly(2023, 7, 10), + ReviewStartsAt = new DateOnly(2023, 7, 1), + ShowDate = new DateOnly(2023, 7, 20), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2023, 6, 30), + VotingStartsAt = new DateOnly(2023, 6, 1), + Year = 2023 + }); + }); + + modelBuilder.Entity("Backend.Domain.SiteSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContactUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("DemoLoginDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginEmail") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("DemoLoginEnabled") + .HasColumnType("boolean"); + + b.Property("DemoLoginManagedByDatabase") + .HasColumnType("boolean"); + + b.Property("DemoLoginPasswordHash") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginPasswordSalt") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DemoLoginTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("FaqJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("HostDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("HostTagline") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("ImprintUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("MaintenanceMessage") + .IsRequired() + .HasMaxLength(600) + .HasColumnType("character varying(600)"); + + b.Property("MaintenanceModeEnabled") + .HasColumnType("boolean"); + + b.Property("MaintenanceTitle") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("NewsletterUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("PrivacyEmail") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("PrivacyPolicyContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("PrivacyPolicyUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivacyPolicyUpdatedBy") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SocialLinksJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.ToTable("SiteSettings"); + + b.HasData( + new + { + Id = 1, + ContactUrl = "https://vtuber-star-awards.de/kontakt", + DemoLoginDisplayName = "Jayuhime Admin", + DemoLoginEmail = "", + DemoLoginEnabled = false, + DemoLoginManagedByDatabase = false, + DemoLoginPasswordHash = "", + DemoLoginPasswordSalt = "", + DemoLoginTwitchUserId = "jayuhime_admin", + FaqJson = "[{\"question\":\"Wer darf nominiert werden?\",\"answer\":\"Jede:r aktive deutschsprachige VTuber kann nominiert werden \\u2014 unabh\\u00E4ngig von Follower-Zahl oder Plattform. Die Community schl\\u00E4gt in der Nominierungsphase ihre Favorit:innen vor.\"},{\"question\":\"Wie funktioniert das Voting?\",\"answer\":\"Du meldest dich ausschlie\\u00DFlich mit deinem Twitch-Account an \\u2014 nur so kannst du teilnehmen. Das h\\u00E4lt das Voting fair und bot-frei. Danach vergibst du pro Kategorie eine Stimme, \\u00E4nderbar bis zum Ende der Phase.\"},{\"question\":\"Was kostet die Teilnahme?\",\"answer\":\"Gar nichts. Nominieren, voten und die Show schauen ist komplett kostenlos \\u2014 der VTuber Star Award ist ein Community-Event von Fans f\\u00FCr Fans.\"},{\"question\":\"Wann und wo findet die Award-Show statt?\",\"answer\":\"Die gro\\u00DFe Live-Show wird von Jayuhime gehostet und auf Twitch \\u0026 YouTube gestreamt. Den genauen Termin findest du im Countdown oben \\u2014 sei live dabei, wenn die Stars gek\\u00FCrt werden!\"},{\"question\":\"Ich wurde nominiert \\u2014 was nun?\",\"answer\":\"Gl\\u00FCckwunsch! Du erh\\u00E4ltst eine Benachrichtigung mit allen Infos. Teile deine Nominierung gern mit deiner Community und ruf zum Voten auf \\u2014 jede Stimme z\\u00E4hlt.\"}]", + HostDisplayName = "Jayuhime", + HostTagline = "VTuber & Award Host", + ImprintUrl = "https://vtuber-star-awards.de/impressum", + MaintenanceMessage = "Die Award-Galaxie wird gerade liebevoll poliert. Schau gleich wieder vorbei.", + MaintenanceModeEnabled = false, + MaintenanceTitle = "Sternenpause", + NewsletterUrl = "https://vtuber-star-awards.de/newsletter", + PrivacyEmail = "datenschutz@vtuber-star-awards.de", + PrivacyPolicyContent = "Verantwortliche:r\nVTuber Star Awards, vertreten durch Jayuhime. Kontakt: datenschutz@vtuber-star-awards.de\n\nWelche Daten wir verarbeiten\nBei Teilnahme (Voting, Nominierung, Clip-Einreichung) verarbeiten wir ausschließlich deine Twitch-User-ID sowie den Zeitstempel deiner Aktion.\nFür Show-Erinnerungen speichern wir optional deine E-Mail-Adresse.\n\nRechtsgrundlage\nVerarbeitung auf Basis von Art. 6 Abs. 1 lit. b DSGVO. Für optionale Erinnerungen gilt Art. 6 Abs. 1 lit. a DSGVO.\n\nZweck der Verarbeitung\nDurchführung des VTuber Star Awards, Sicherstellung fairer Abstimmung, Spam-Prävention sowie Admin-Review von Einreichungen.\n\nLöschfristen\nAlle Teilnahmedaten werden spätestens 6 Monate nach der Award-Show automatisch gelöscht. E-Mail-Adressen werden nach der Show entfernt.\n\nDeine Rechte\nDu hast das Recht auf Auskunft, Löschung, Berichtigung, Widerspruch und Beschwerde bei der zuständigen Aufsichtsbehörde.\n\nWeitergabe an Dritte\nKeine Weitergabe zu Werbezwecken. Technische Dienstleister verarbeiten Daten ausschließlich im Rahmen der Auftragsverarbeitung.", + PrivacyPolicyUpdatedAt = new DateTimeOffset(new DateTime(2026, 6, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + PrivacyPolicyUpdatedBy = "seed", + SocialLinksJson = "[{\"label\":\"Twitch\",\"platform\":\"twitch\",\"url\":\"https://twitch.tv/jayuhime\",\"icon\":\"twitch\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"YouTube\",\"platform\":\"youtube\",\"url\":\"https://youtube.com/c/Jayuhime\",\"icon\":\"youtube\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"X\",\"platform\":\"x\",\"url\":\"https://x.com/jayuhime\",\"icon\":\"x\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Instagram\",\"platform\":\"instagram\",\"url\":\"https://instagram.com/jayuhime\",\"icon\":\"instagram\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Discord\",\"platform\":\"discord\",\"url\":\"https://discord.gg/jayuhime\",\"icon\":\"discord\",\"showOnHost\":true,\"showOnCommunity\":true}]", + SponsorsUrl = "https://vtuber-star-awards.de/partner" + }); + }); + + modelBuilder.Entity("Backend.Domain.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SessionToken") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SessionToken") + .IsUnique(); + + b.ToTable("UserSessions"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("VoteBallots"); + + b.HasData( + new + { + Id = 1, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_1" + }, + new + { + Id = 2, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 5, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_2" + }); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BallotId") + .HasColumnType("integer"); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BallotId"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.ToTable("VoteEntries"); + + b.HasData( + new + { + Id = 1, + BallotId = 1, + CandidateId = 1, + CategoryId = 1 + }, + new + { + Id = 2, + BallotId = 1, + CandidateId = 4, + CategoryId = 2 + }, + new + { + Id = 3, + BallotId = 2, + CandidateId = 2, + CategoryId = 1 + }, + new + { + Id = 4, + BallotId = 2, + CandidateId = 6, + CategoryId = 3 + }); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Results") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.HasOne("Backend.Domain.Category", "Category") + .WithMany("Candidates") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Categories") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId"); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.HasOne("Backend.Domain.VoteBallot", "Ballot") + .WithMany("Entries") + .HasForeignKey("BallotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ballot"); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Navigation("Candidates"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Navigation("Categories"); + + b.Navigation("Results"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Navigation("Entries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/Migrations/20260624065545_AddOperationalSiteSettings.cs b/Backend/Migrations/20260624065545_AddOperationalSiteSettings.cs new file mode 100644 index 0000000..c6b1e0b --- /dev/null +++ b/Backend/Migrations/20260624065545_AddOperationalSiteSettings.cs @@ -0,0 +1,143 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backend.Migrations +{ + /// + public partial class AddOperationalSiteSettings : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DemoLoginDisplayName", + table: "SiteSettings", + type: "character varying(120)", + maxLength: 120, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "DemoLoginEmail", + table: "SiteSettings", + type: "character varying(180)", + maxLength: 180, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "DemoLoginEnabled", + table: "SiteSettings", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "DemoLoginManagedByDatabase", + table: "SiteSettings", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "DemoLoginPasswordHash", + table: "SiteSettings", + type: "character varying(120)", + maxLength: 120, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "DemoLoginPasswordSalt", + table: "SiteSettings", + type: "character varying(80)", + maxLength: 80, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "DemoLoginTwitchUserId", + table: "SiteSettings", + type: "character varying(120)", + maxLength: 120, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "MaintenanceMessage", + table: "SiteSettings", + type: "character varying(600)", + maxLength: 600, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "MaintenanceModeEnabled", + table: "SiteSettings", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "MaintenanceTitle", + table: "SiteSettings", + type: "character varying(120)", + maxLength: 120, + nullable: false, + defaultValue: ""); + + migrationBuilder.UpdateData( + table: "SiteSettings", + keyColumn: "Id", + keyValue: 1, + columns: new[] { "DemoLoginDisplayName", "DemoLoginEmail", "DemoLoginEnabled", "DemoLoginManagedByDatabase", "DemoLoginPasswordHash", "DemoLoginPasswordSalt", "DemoLoginTwitchUserId", "MaintenanceMessage", "MaintenanceModeEnabled", "MaintenanceTitle" }, + values: new object[] { "Jayuhime Admin", "", false, false, "", "", "jayuhime_admin", "Die Award-Galaxie wird gerade liebevoll poliert. Schau gleich wieder vorbei.", false, "Sternenpause" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DemoLoginDisplayName", + table: "SiteSettings"); + + migrationBuilder.DropColumn( + name: "DemoLoginEmail", + table: "SiteSettings"); + + migrationBuilder.DropColumn( + name: "DemoLoginEnabled", + table: "SiteSettings"); + + migrationBuilder.DropColumn( + name: "DemoLoginManagedByDatabase", + table: "SiteSettings"); + + migrationBuilder.DropColumn( + name: "DemoLoginPasswordHash", + table: "SiteSettings"); + + migrationBuilder.DropColumn( + name: "DemoLoginPasswordSalt", + table: "SiteSettings"); + + migrationBuilder.DropColumn( + name: "DemoLoginTwitchUserId", + table: "SiteSettings"); + + migrationBuilder.DropColumn( + name: "MaintenanceMessage", + table: "SiteSettings"); + + migrationBuilder.DropColumn( + name: "MaintenanceModeEnabled", + table: "SiteSettings"); + + migrationBuilder.DropColumn( + name: "MaintenanceTitle", + table: "SiteSettings"); + + } + } +} diff --git a/Backend/Migrations/20260624133211_AddSeasonShowStartsAt.Designer.cs b/Backend/Migrations/20260624133211_AddSeasonShowStartsAt.Designer.cs new file mode 100644 index 0000000..43b2ad4 --- /dev/null +++ b/Backend/Migrations/20260624133211_AddSeasonShowStartsAt.Designer.cs @@ -0,0 +1,1274 @@ +// +using System; +using Backend.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backend.Migrations +{ + [DbContext(typeof(AwardsDbContext))] + [Migration("20260624133211_AddSeasonShowStartsAt")] + partial class AddSeasonShowStartsAt + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backend.Domain.AdminAuditEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("AdminTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.HasKey("Id"); + + b.ToTable("AdminAuditEntries"); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CategoryName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "CategoryId") + .IsUnique(); + + b.ToTable("Results"); + + b.HasData( + new + { + Id = 1, + CandidateId = 8, + CategoryId = 5, + CategoryName = "VTuber des Jahres", + SeasonId = 2 + }, + new + { + Id = 2, + CandidateId = 9, + CategoryId = 6, + CategoryName = "Bestes Live Event", + SeasonId = 2 + }, + new + { + Id = 3, + CandidateId = 10, + CategoryId = 7, + CategoryName = "Clip des Jahres", + SeasonId = 2 + }, + new + { + Id = 4, + CandidateId = 11, + CategoryId = 8, + CategoryName = "VTuber des Jahres", + SeasonId = 3 + }, + new + { + Id = 5, + CandidateId = 12, + CategoryId = 9, + CategoryName = "Clip des Jahres", + SeasonId = 3 + }, + new + { + Id = 6, + CandidateId = 13, + CategoryId = 10, + CategoryName = "VTuber des Jahres", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ChannelSlug") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId"); + + b.ToTable("Candidates"); + + b.HasData( + new + { + Id = 1, + CategoryId = 1, + ChannelSlug = "@hoshimimiyu", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 2, + CategoryId = 1, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 3, + CategoryId = 1, + ChannelSlug = "@shiroch", + DisplayName = "Shiro Ch.", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 4, + CategoryId = 2, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 5, + CategoryId = 2, + ChannelSlug = "@aoisakura", + DisplayName = "Aoi Sakura Showcase", + Platform = "YouTube", + SeasonId = 1 + }, + new + { + Id = 6, + CategoryId = 3, + ChannelSlug = "@pyonkichikingdom", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 7, + CategoryId = 4, + ChannelSlug = "@moonrelay", + DisplayName = "Moonrelay", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 8, + CategoryId = 5, + ChannelSlug = "@hoshimimiyu", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 9, + CategoryId = 6, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 10, + CategoryId = 7, + ChannelSlug = "@pyonkichikingdom", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 11, + CategoryId = 8, + ChannelSlug = "@aoisakura", + DisplayName = "Aoi Sakura", + Platform = "YouTube", + SeasonId = 3 + }, + new + { + Id = 12, + CategoryId = 9, + ChannelSlug = "@starbyte", + DisplayName = "Starbyte", + Platform = "Twitch", + SeasonId = 3 + }, + new + { + Id = 13, + CategoryId = 10, + ChannelSlug = "@tenshivox", + DisplayName = "Tenshi Vox", + Platform = "Twitch", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MaxNomineesPerUser") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Slug") + .IsUnique(); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = 1, + Description = "Die groesste Auszeichnung des Jahres.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 1, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 2, + Description = "Events, Konzerte und 3D-Shows.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 1, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 3, + Description = "Der lustigste oder emotionalste Clip des Jahres.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 1, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 4, + Description = "Die aktivste und freundlichste Community.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "Beste Community", + SeasonId = 1, + Slug = "beste-community", + SortOrder = 4 + }, + new + { + Id = 5, + Description = "Archivkategorie 2025.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 2, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 6, + Description = "Archivkategorie 2025.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 2, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 7, + Description = "Archivkategorie 2025.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 2, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 8, + Description = "Archivkategorie 2024.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 3, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 9, + Description = "Archivkategorie 2024.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 3, + Slug = "clip-des-jahres", + SortOrder = 2 + }, + new + { + Id = 10, + Description = "Archivkategorie 2023.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 4, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ClipUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Creator") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("ClipSubmissions"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CandidateText") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("Nominations"); + + b.HasData( + new + { + Id = 1, + CandidateText = "Hoshimi Miyu", + CategoryId = 1, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 13, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_hoshi" + }, + new + { + Id = 2, + CandidateText = "Kurainu 3D Live", + CategoryId = 2, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 14, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_kurainu" + }); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("TwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("RiskFlags"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrentPhase") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("IsCommunityOnly") + .HasColumnType("boolean"); + + b.Property("IsCurrent") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("NominationEndsAt") + .HasColumnType("date"); + + b.Property("NominationStartsAt") + .HasColumnType("date"); + + b.Property("ReviewEndsAt") + .HasColumnType("date"); + + b.Property("ReviewStartsAt") + .HasColumnType("date"); + + b.Property("ShowDate") + .HasColumnType("date"); + + b.Property("ShowStartsAt") + .HasColumnType("time without time zone"); + + b.Property("ShowStreamUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("VotingEndsAt") + .HasColumnType("date"); + + b.Property("VotingStartsAt") + .HasColumnType("date"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("Seasons"); + + b.HasData( + new + { + Id = 1, + CurrentPhase = "Community Voting", + IsCommunityOnly = true, + IsCurrent = true, + Name = "VTuber Star Awards 2026", + NominationEndsAt = new DateOnly(2026, 5, 31), + NominationStartsAt = new DateOnly(2026, 5, 1), + ReviewEndsAt = new DateOnly(2026, 7, 10), + ReviewStartsAt = new DateOnly(2026, 7, 1), + ShowDate = new DateOnly(2026, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2026, 6, 30), + VotingStartsAt = new DateOnly(2026, 6, 1), + Year = 2026 + }, + new + { + Id = 2, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2025", + NominationEndsAt = new DateOnly(2025, 5, 31), + NominationStartsAt = new DateOnly(2025, 5, 1), + ReviewEndsAt = new DateOnly(2025, 7, 10), + ReviewStartsAt = new DateOnly(2025, 7, 1), + ShowDate = new DateOnly(2025, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2025, 6, 30), + VotingStartsAt = new DateOnly(2025, 6, 1), + Year = 2025 + }, + new + { + Id = 3, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2024", + NominationEndsAt = new DateOnly(2024, 5, 31), + NominationStartsAt = new DateOnly(2024, 5, 1), + ReviewEndsAt = new DateOnly(2024, 7, 10), + ReviewStartsAt = new DateOnly(2024, 7, 1), + ShowDate = new DateOnly(2024, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://youtube.com/c/Jayuhime", + VotingEndsAt = new DateOnly(2024, 6, 30), + VotingStartsAt = new DateOnly(2024, 6, 1), + Year = 2024 + }, + new + { + Id = 4, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2023", + NominationEndsAt = new DateOnly(2023, 5, 31), + NominationStartsAt = new DateOnly(2023, 5, 1), + ReviewEndsAt = new DateOnly(2023, 7, 10), + ReviewStartsAt = new DateOnly(2023, 7, 1), + ShowDate = new DateOnly(2023, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2023, 6, 30), + VotingStartsAt = new DateOnly(2023, 6, 1), + Year = 2023 + }); + }); + + modelBuilder.Entity("Backend.Domain.SiteSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContactUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("DemoLoginDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginEmail") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("DemoLoginEnabled") + .HasColumnType("boolean"); + + b.Property("DemoLoginManagedByDatabase") + .HasColumnType("boolean"); + + b.Property("DemoLoginPasswordHash") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginPasswordSalt") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DemoLoginTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("FaqJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("HostDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("HostTagline") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("ImprintUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("MaintenanceMessage") + .IsRequired() + .HasMaxLength(600) + .HasColumnType("character varying(600)"); + + b.Property("MaintenanceModeEnabled") + .HasColumnType("boolean"); + + b.Property("MaintenanceTitle") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("NewsletterUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("PrivacyEmail") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("PrivacyPolicyContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("PrivacyPolicyUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivacyPolicyUpdatedBy") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SocialLinksJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.ToTable("SiteSettings"); + + b.HasData( + new + { + Id = 1, + ContactUrl = "https://vtuber-star-awards.de/kontakt", + DemoLoginDisplayName = "Jayuhime Admin", + DemoLoginEmail = "", + DemoLoginEnabled = false, + DemoLoginManagedByDatabase = false, + DemoLoginPasswordHash = "", + DemoLoginPasswordSalt = "", + DemoLoginTwitchUserId = "jayuhime_admin", + FaqJson = "[{\"question\":\"Wer darf nominiert werden?\",\"answer\":\"Jede:r aktive deutschsprachige VTuber kann nominiert werden \\u2014 unabh\\u00E4ngig von Follower-Zahl oder Plattform. Die Community schl\\u00E4gt in der Nominierungsphase ihre Favorit:innen vor.\"},{\"question\":\"Wie funktioniert das Voting?\",\"answer\":\"Du meldest dich ausschlie\\u00DFlich mit deinem Twitch-Account an \\u2014 nur so kannst du teilnehmen. Das h\\u00E4lt das Voting fair und bot-frei. Danach vergibst du pro Kategorie eine Stimme, \\u00E4nderbar bis zum Ende der Phase.\"},{\"question\":\"Was kostet die Teilnahme?\",\"answer\":\"Gar nichts. Nominieren, voten und die Show schauen ist komplett kostenlos \\u2014 der VTuber Star Award ist ein Community-Event von Fans f\\u00FCr Fans.\"},{\"question\":\"Wann und wo findet die Award-Show statt?\",\"answer\":\"Die gro\\u00DFe Live-Show wird von Jayuhime gehostet und auf Twitch \\u0026 YouTube gestreamt. Den genauen Termin findest du im Countdown oben \\u2014 sei live dabei, wenn die Stars gek\\u00FCrt werden!\"},{\"question\":\"Ich wurde nominiert \\u2014 was nun?\",\"answer\":\"Gl\\u00FCckwunsch! Du erh\\u00E4ltst eine Benachrichtigung mit allen Infos. Teile deine Nominierung gern mit deiner Community und ruf zum Voten auf \\u2014 jede Stimme z\\u00E4hlt.\"}]", + HostDisplayName = "Jayuhime", + HostTagline = "VTuber & Award Host", + ImprintUrl = "https://vtuber-star-awards.de/impressum", + MaintenanceMessage = "Die Award-Galaxie wird gerade liebevoll poliert. Schau gleich wieder vorbei.", + MaintenanceModeEnabled = false, + MaintenanceTitle = "Sternenpause", + NewsletterUrl = "https://vtuber-star-awards.de/newsletter", + PrivacyEmail = "datenschutz@vtuber-star-awards.de", + PrivacyPolicyContent = "Verantwortliche:r\nVTuber Star Awards, vertreten durch Jayuhime. Kontakt: datenschutz@vtuber-star-awards.de\n\nWelche Daten wir verarbeiten\nBei Teilnahme (Voting, Nominierung, Clip-Einreichung) verarbeiten wir ausschließlich deine Twitch-User-ID sowie den Zeitstempel deiner Aktion.\nFür Show-Erinnerungen speichern wir optional deine E-Mail-Adresse.\n\nRechtsgrundlage\nVerarbeitung auf Basis von Art. 6 Abs. 1 lit. b DSGVO. Für optionale Erinnerungen gilt Art. 6 Abs. 1 lit. a DSGVO.\n\nZweck der Verarbeitung\nDurchführung des VTuber Star Awards, Sicherstellung fairer Abstimmung, Spam-Prävention sowie Admin-Review von Einreichungen.\n\nLöschfristen\nAlle Teilnahmedaten werden spätestens 6 Monate nach der Award-Show automatisch gelöscht. E-Mail-Adressen werden nach der Show entfernt.\n\nDeine Rechte\nDu hast das Recht auf Auskunft, Löschung, Berichtigung, Widerspruch und Beschwerde bei der zuständigen Aufsichtsbehörde.\n\nWeitergabe an Dritte\nKeine Weitergabe zu Werbezwecken. Technische Dienstleister verarbeiten Daten ausschließlich im Rahmen der Auftragsverarbeitung.", + PrivacyPolicyUpdatedAt = new DateTimeOffset(new DateTime(2026, 6, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + PrivacyPolicyUpdatedBy = "seed", + SocialLinksJson = "[{\"label\":\"Twitch\",\"platform\":\"twitch\",\"url\":\"https://twitch.tv/jayuhime\",\"icon\":\"twitch\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"YouTube\",\"platform\":\"youtube\",\"url\":\"https://youtube.com/c/Jayuhime\",\"icon\":\"youtube\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"X\",\"platform\":\"x\",\"url\":\"https://x.com/jayuhime\",\"icon\":\"x\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Instagram\",\"platform\":\"instagram\",\"url\":\"https://instagram.com/jayuhime\",\"icon\":\"instagram\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Discord\",\"platform\":\"discord\",\"url\":\"https://discord.gg/jayuhime\",\"icon\":\"discord\",\"showOnHost\":true,\"showOnCommunity\":true}]", + SponsorsUrl = "https://vtuber-star-awards.de/partner" + }); + }); + + modelBuilder.Entity("Backend.Domain.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SessionToken") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SessionToken") + .IsUnique(); + + b.ToTable("UserSessions"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("VoteBallots"); + + b.HasData( + new + { + Id = 1, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_1" + }, + new + { + Id = 2, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 5, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_2" + }); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BallotId") + .HasColumnType("integer"); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BallotId"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.ToTable("VoteEntries"); + + b.HasData( + new + { + Id = 1, + BallotId = 1, + CandidateId = 1, + CategoryId = 1 + }, + new + { + Id = 2, + BallotId = 1, + CandidateId = 4, + CategoryId = 2 + }, + new + { + Id = 3, + BallotId = 2, + CandidateId = 2, + CategoryId = 1 + }, + new + { + Id = 4, + BallotId = 2, + CandidateId = 6, + CategoryId = 3 + }); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Results") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.HasOne("Backend.Domain.Category", "Category") + .WithMany("Candidates") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Categories") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId"); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.HasOne("Backend.Domain.VoteBallot", "Ballot") + .WithMany("Entries") + .HasForeignKey("BallotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ballot"); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Navigation("Candidates"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Navigation("Categories"); + + b.Navigation("Results"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Navigation("Entries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/Migrations/20260624133211_AddSeasonShowStartsAt.cs b/Backend/Migrations/20260624133211_AddSeasonShowStartsAt.cs new file mode 100644 index 0000000..d9e45c3 --- /dev/null +++ b/Backend/Migrations/20260624133211_AddSeasonShowStartsAt.cs @@ -0,0 +1,57 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backend.Migrations +{ + /// + public partial class AddSeasonShowStartsAt : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql( + """ + ALTER TABLE "Seasons" + ADD COLUMN IF NOT EXISTS "ShowStartsAt" time without time zone NOT NULL DEFAULT TIME '20:00:00'; + """); + + migrationBuilder.UpdateData( + table: "Seasons", + keyColumn: "Id", + keyValue: 1, + column: "ShowStartsAt", + value: new TimeOnly(20, 0, 0)); + + migrationBuilder.UpdateData( + table: "Seasons", + keyColumn: "Id", + keyValue: 2, + column: "ShowStartsAt", + value: new TimeOnly(20, 0, 0)); + + migrationBuilder.UpdateData( + table: "Seasons", + keyColumn: "Id", + keyValue: 3, + column: "ShowStartsAt", + value: new TimeOnly(20, 0, 0)); + + migrationBuilder.UpdateData( + table: "Seasons", + keyColumn: "Id", + keyValue: 4, + column: "ShowStartsAt", + value: new TimeOnly(20, 0, 0)); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ShowStartsAt", + table: "Seasons"); + } + } +} diff --git a/Backend/Migrations/20260624134432_AddClipCandidateLink.Designer.cs b/Backend/Migrations/20260624134432_AddClipCandidateLink.Designer.cs new file mode 100644 index 0000000..0e53fcb --- /dev/null +++ b/Backend/Migrations/20260624134432_AddClipCandidateLink.Designer.cs @@ -0,0 +1,1289 @@ +// +using System; +using Backend.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backend.Migrations +{ + [DbContext(typeof(AwardsDbContext))] + [Migration("20260624134432_AddClipCandidateLink")] + partial class AddClipCandidateLink + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backend.Domain.AdminAuditEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("AdminTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.HasKey("Id"); + + b.ToTable("AdminAuditEntries"); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CategoryName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "CategoryId") + .IsUnique(); + + b.ToTable("Results"); + + b.HasData( + new + { + Id = 1, + CandidateId = 8, + CategoryId = 5, + CategoryName = "VTuber des Jahres", + SeasonId = 2 + }, + new + { + Id = 2, + CandidateId = 9, + CategoryId = 6, + CategoryName = "Bestes Live Event", + SeasonId = 2 + }, + new + { + Id = 3, + CandidateId = 10, + CategoryId = 7, + CategoryName = "Clip des Jahres", + SeasonId = 2 + }, + new + { + Id = 4, + CandidateId = 11, + CategoryId = 8, + CategoryName = "VTuber des Jahres", + SeasonId = 3 + }, + new + { + Id = 5, + CandidateId = 12, + CategoryId = 9, + CategoryName = "Clip des Jahres", + SeasonId = 3 + }, + new + { + Id = 6, + CandidateId = 13, + CategoryId = 10, + CategoryName = "VTuber des Jahres", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ChannelSlug") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId"); + + b.ToTable("Candidates"); + + b.HasData( + new + { + Id = 1, + CategoryId = 1, + ChannelSlug = "@hoshimimiyu", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 2, + CategoryId = 1, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 3, + CategoryId = 1, + ChannelSlug = "@shiroch", + DisplayName = "Shiro Ch.", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 4, + CategoryId = 2, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 5, + CategoryId = 2, + ChannelSlug = "@aoisakura", + DisplayName = "Aoi Sakura Showcase", + Platform = "YouTube", + SeasonId = 1 + }, + new + { + Id = 6, + CategoryId = 3, + ChannelSlug = "@pyonkichikingdom", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 7, + CategoryId = 4, + ChannelSlug = "@moonrelay", + DisplayName = "Moonrelay", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 8, + CategoryId = 5, + ChannelSlug = "@hoshimimiyu", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 9, + CategoryId = 6, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 10, + CategoryId = 7, + ChannelSlug = "@pyonkichikingdom", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 11, + CategoryId = 8, + ChannelSlug = "@aoisakura", + DisplayName = "Aoi Sakura", + Platform = "YouTube", + SeasonId = 3 + }, + new + { + Id = 12, + CategoryId = 9, + ChannelSlug = "@starbyte", + DisplayName = "Starbyte", + Platform = "Twitch", + SeasonId = 3 + }, + new + { + Id = 13, + CategoryId = 10, + ChannelSlug = "@tenshivox", + DisplayName = "Tenshi Vox", + Platform = "Twitch", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MaxNomineesPerUser") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Slug") + .IsUnique(); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = 1, + Description = "Die groesste Auszeichnung des Jahres.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 1, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 2, + Description = "Events, Konzerte und 3D-Shows.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 1, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 3, + Description = "Der lustigste oder emotionalste Clip des Jahres.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 1, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 4, + Description = "Die aktivste und freundlichste Community.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "Beste Community", + SeasonId = 1, + Slug = "beste-community", + SortOrder = 4 + }, + new + { + Id = 5, + Description = "Archivkategorie 2025.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 2, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 6, + Description = "Archivkategorie 2025.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 2, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 7, + Description = "Archivkategorie 2025.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 2, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 8, + Description = "Archivkategorie 2024.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 3, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 9, + Description = "Archivkategorie 2024.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 3, + Slug = "clip-des-jahres", + SortOrder = 2 + }, + new + { + Id = 10, + Description = "Archivkategorie 2023.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 4, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ClipUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Creator") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("ClipSubmissions"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CandidateText") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("Nominations"); + + b.HasData( + new + { + Id = 1, + CandidateText = "Hoshimi Miyu", + CategoryId = 1, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 13, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_hoshi" + }, + new + { + Id = 2, + CandidateText = "Kurainu 3D Live", + CategoryId = 2, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 14, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_kurainu" + }); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("TwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("RiskFlags"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrentPhase") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("IsCommunityOnly") + .HasColumnType("boolean"); + + b.Property("IsCurrent") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("NominationEndsAt") + .HasColumnType("date"); + + b.Property("NominationStartsAt") + .HasColumnType("date"); + + b.Property("ReviewEndsAt") + .HasColumnType("date"); + + b.Property("ReviewStartsAt") + .HasColumnType("date"); + + b.Property("ShowDate") + .HasColumnType("date"); + + b.Property("ShowStartsAt") + .HasColumnType("time without time zone"); + + b.Property("ShowStreamUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("VotingEndsAt") + .HasColumnType("date"); + + b.Property("VotingStartsAt") + .HasColumnType("date"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("Seasons"); + + b.HasData( + new + { + Id = 1, + CurrentPhase = "Community Voting", + IsCommunityOnly = true, + IsCurrent = true, + Name = "VTuber Star Awards 2026", + NominationEndsAt = new DateOnly(2026, 5, 31), + NominationStartsAt = new DateOnly(2026, 5, 1), + ReviewEndsAt = new DateOnly(2026, 7, 10), + ReviewStartsAt = new DateOnly(2026, 7, 1), + ShowDate = new DateOnly(2026, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2026, 6, 30), + VotingStartsAt = new DateOnly(2026, 6, 1), + Year = 2026 + }, + new + { + Id = 2, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2025", + NominationEndsAt = new DateOnly(2025, 5, 31), + NominationStartsAt = new DateOnly(2025, 5, 1), + ReviewEndsAt = new DateOnly(2025, 7, 10), + ReviewStartsAt = new DateOnly(2025, 7, 1), + ShowDate = new DateOnly(2025, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2025, 6, 30), + VotingStartsAt = new DateOnly(2025, 6, 1), + Year = 2025 + }, + new + { + Id = 3, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2024", + NominationEndsAt = new DateOnly(2024, 5, 31), + NominationStartsAt = new DateOnly(2024, 5, 1), + ReviewEndsAt = new DateOnly(2024, 7, 10), + ReviewStartsAt = new DateOnly(2024, 7, 1), + ShowDate = new DateOnly(2024, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://youtube.com/c/Jayuhime", + VotingEndsAt = new DateOnly(2024, 6, 30), + VotingStartsAt = new DateOnly(2024, 6, 1), + Year = 2024 + }, + new + { + Id = 4, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2023", + NominationEndsAt = new DateOnly(2023, 5, 31), + NominationStartsAt = new DateOnly(2023, 5, 1), + ReviewEndsAt = new DateOnly(2023, 7, 10), + ReviewStartsAt = new DateOnly(2023, 7, 1), + ShowDate = new DateOnly(2023, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2023, 6, 30), + VotingStartsAt = new DateOnly(2023, 6, 1), + Year = 2023 + }); + }); + + modelBuilder.Entity("Backend.Domain.SiteSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContactUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("DemoLoginDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginEmail") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("DemoLoginEnabled") + .HasColumnType("boolean"); + + b.Property("DemoLoginManagedByDatabase") + .HasColumnType("boolean"); + + b.Property("DemoLoginPasswordHash") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginPasswordSalt") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DemoLoginTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("FaqJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("HostDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("HostTagline") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("ImprintUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("MaintenanceMessage") + .IsRequired() + .HasMaxLength(600) + .HasColumnType("character varying(600)"); + + b.Property("MaintenanceModeEnabled") + .HasColumnType("boolean"); + + b.Property("MaintenanceTitle") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("NewsletterUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("PrivacyEmail") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("PrivacyPolicyContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("PrivacyPolicyUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivacyPolicyUpdatedBy") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SocialLinksJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.ToTable("SiteSettings"); + + b.HasData( + new + { + Id = 1, + ContactUrl = "https://vtuber-star-awards.de/kontakt", + DemoLoginDisplayName = "Jayuhime Admin", + DemoLoginEmail = "", + DemoLoginEnabled = false, + DemoLoginManagedByDatabase = false, + DemoLoginPasswordHash = "", + DemoLoginPasswordSalt = "", + DemoLoginTwitchUserId = "jayuhime_admin", + FaqJson = "[{\"question\":\"Wer darf nominiert werden?\",\"answer\":\"Jede:r aktive deutschsprachige VTuber kann nominiert werden \\u2014 unabh\\u00E4ngig von Follower-Zahl oder Plattform. Die Community schl\\u00E4gt in der Nominierungsphase ihre Favorit:innen vor.\"},{\"question\":\"Wie funktioniert das Voting?\",\"answer\":\"Du meldest dich ausschlie\\u00DFlich mit deinem Twitch-Account an \\u2014 nur so kannst du teilnehmen. Das h\\u00E4lt das Voting fair und bot-frei. Danach vergibst du pro Kategorie eine Stimme, \\u00E4nderbar bis zum Ende der Phase.\"},{\"question\":\"Was kostet die Teilnahme?\",\"answer\":\"Gar nichts. Nominieren, voten und die Show schauen ist komplett kostenlos \\u2014 der VTuber Star Award ist ein Community-Event von Fans f\\u00FCr Fans.\"},{\"question\":\"Wann und wo findet die Award-Show statt?\",\"answer\":\"Die gro\\u00DFe Live-Show wird von Jayuhime gehostet und auf Twitch \\u0026 YouTube gestreamt. Den genauen Termin findest du im Countdown oben \\u2014 sei live dabei, wenn die Stars gek\\u00FCrt werden!\"},{\"question\":\"Ich wurde nominiert \\u2014 was nun?\",\"answer\":\"Gl\\u00FCckwunsch! Du erh\\u00E4ltst eine Benachrichtigung mit allen Infos. Teile deine Nominierung gern mit deiner Community und ruf zum Voten auf \\u2014 jede Stimme z\\u00E4hlt.\"}]", + HostDisplayName = "Jayuhime", + HostTagline = "VTuber & Award Host", + ImprintUrl = "https://vtuber-star-awards.de/impressum", + MaintenanceMessage = "Die Award-Galaxie wird gerade liebevoll poliert. Schau gleich wieder vorbei.", + MaintenanceModeEnabled = false, + MaintenanceTitle = "Sternenpause", + NewsletterUrl = "https://vtuber-star-awards.de/newsletter", + PrivacyEmail = "datenschutz@vtuber-star-awards.de", + PrivacyPolicyContent = "Verantwortliche:r\nVTuber Star Awards, vertreten durch Jayuhime. Kontakt: datenschutz@vtuber-star-awards.de\n\nWelche Daten wir verarbeiten\nBei Teilnahme (Voting, Nominierung, Clip-Einreichung) verarbeiten wir ausschließlich deine Twitch-User-ID sowie den Zeitstempel deiner Aktion.\nFür Show-Erinnerungen speichern wir optional deine E-Mail-Adresse.\n\nRechtsgrundlage\nVerarbeitung auf Basis von Art. 6 Abs. 1 lit. b DSGVO. Für optionale Erinnerungen gilt Art. 6 Abs. 1 lit. a DSGVO.\n\nZweck der Verarbeitung\nDurchführung des VTuber Star Awards, Sicherstellung fairer Abstimmung, Spam-Prävention sowie Admin-Review von Einreichungen.\n\nLöschfristen\nAlle Teilnahmedaten werden spätestens 6 Monate nach der Award-Show automatisch gelöscht. E-Mail-Adressen werden nach der Show entfernt.\n\nDeine Rechte\nDu hast das Recht auf Auskunft, Löschung, Berichtigung, Widerspruch und Beschwerde bei der zuständigen Aufsichtsbehörde.\n\nWeitergabe an Dritte\nKeine Weitergabe zu Werbezwecken. Technische Dienstleister verarbeiten Daten ausschließlich im Rahmen der Auftragsverarbeitung.", + PrivacyPolicyUpdatedAt = new DateTimeOffset(new DateTime(2026, 6, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + PrivacyPolicyUpdatedBy = "seed", + SocialLinksJson = "[{\"label\":\"Twitch\",\"platform\":\"twitch\",\"url\":\"https://twitch.tv/jayuhime\",\"icon\":\"twitch\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"YouTube\",\"platform\":\"youtube\",\"url\":\"https://youtube.com/c/Jayuhime\",\"icon\":\"youtube\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"X\",\"platform\":\"x\",\"url\":\"https://x.com/jayuhime\",\"icon\":\"x\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Instagram\",\"platform\":\"instagram\",\"url\":\"https://instagram.com/jayuhime\",\"icon\":\"instagram\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Discord\",\"platform\":\"discord\",\"url\":\"https://discord.gg/jayuhime\",\"icon\":\"discord\",\"showOnHost\":true,\"showOnCommunity\":true}]", + SponsorsUrl = "https://vtuber-star-awards.de/partner" + }); + }); + + modelBuilder.Entity("Backend.Domain.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SessionToken") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SessionToken") + .IsUnique(); + + b.ToTable("UserSessions"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("VoteBallots"); + + b.HasData( + new + { + Id = 1, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_1" + }, + new + { + Id = 2, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 5, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_2" + }); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BallotId") + .HasColumnType("integer"); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BallotId"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.ToTable("VoteEntries"); + + b.HasData( + new + { + Id = 1, + BallotId = 1, + CandidateId = 1, + CategoryId = 1 + }, + new + { + Id = 2, + BallotId = 1, + CandidateId = 4, + CategoryId = 2 + }, + new + { + Id = 3, + BallotId = 2, + CandidateId = 2, + CategoryId = 1 + }, + new + { + Id = 4, + BallotId = 2, + CandidateId = 6, + CategoryId = 3 + }); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Results") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.HasOne("Backend.Domain.Category", "Category") + .WithMany("Candidates") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Categories") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Candidate"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId"); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.HasOne("Backend.Domain.VoteBallot", "Ballot") + .WithMany("Entries") + .HasForeignKey("BallotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ballot"); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Navigation("Candidates"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Navigation("Categories"); + + b.Navigation("Results"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Navigation("Entries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/Migrations/20260624134432_AddClipCandidateLink.cs b/Backend/Migrations/20260624134432_AddClipCandidateLink.cs new file mode 100644 index 0000000..3e4aac3 --- /dev/null +++ b/Backend/Migrations/20260624134432_AddClipCandidateLink.cs @@ -0,0 +1,54 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backend.Migrations +{ + /// + public partial class AddClipCandidateLink : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql( + """ + ALTER TABLE IF EXISTS "ClipSubmissions" + ADD COLUMN IF NOT EXISTS "CandidateId" integer NULL; + + DO $$ + BEGIN + IF to_regclass('"ClipSubmissions"') IS NOT NULL THEN + CREATE INDEX IF NOT EXISTS "IX_ClipSubmissions_CandidateId" + ON "ClipSubmissions" ("CandidateId"); + + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'FK_ClipSubmissions_Candidates_CandidateId' + ) THEN + ALTER TABLE "ClipSubmissions" + ADD CONSTRAINT "FK_ClipSubmissions_Candidates_CandidateId" + FOREIGN KEY ("CandidateId") REFERENCES "Candidates" ("Id") + ON DELETE SET NULL; + END IF; + END IF; + END $$; + """); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql( + """ + ALTER TABLE IF EXISTS "ClipSubmissions" + DROP CONSTRAINT IF EXISTS "FK_ClipSubmissions_Candidates_CandidateId"; + + DROP INDEX IF EXISTS "IX_ClipSubmissions_CandidateId"; + + ALTER TABLE IF EXISTS "ClipSubmissions" + DROP COLUMN IF EXISTS "CandidateId"; + """); + } + } +} diff --git a/Backend/Migrations/20260624150500_AddRiskFlagReviewNote.cs b/Backend/Migrations/20260624150500_AddRiskFlagReviewNote.cs new file mode 100644 index 0000000..1ad1bfe --- /dev/null +++ b/Backend/Migrations/20260624150500_AddRiskFlagReviewNote.cs @@ -0,0 +1,34 @@ +using Backend.Data; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backend.Migrations +{ + /// + [DbContext(typeof(AwardsDbContext))] + [Migration("20260624150500_AddRiskFlagReviewNote")] + public partial class AddRiskFlagReviewNote : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql( + """ + ALTER TABLE IF EXISTS "RiskFlags" + ADD COLUMN IF NOT EXISTS "ReviewNote" character varying(500) NULL; + """); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql( + """ + ALTER TABLE IF EXISTS "RiskFlags" + DROP COLUMN IF EXISTS "ReviewNote"; + """); + } + } +} diff --git a/Backend/Migrations/20260624162000_AddRiskRulesJson.cs b/Backend/Migrations/20260624162000_AddRiskRulesJson.cs new file mode 100644 index 0000000..275c859 --- /dev/null +++ b/Backend/Migrations/20260624162000_AddRiskRulesJson.cs @@ -0,0 +1,39 @@ +using Backend.Data; +using Backend.Services; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backend.Migrations +{ + /// + [DbContext(typeof(AwardsDbContext))] + [Migration("20260624162000_AddRiskRulesJson")] + public partial class AddRiskRulesJson : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql( + $""" + ALTER TABLE IF EXISTS "SiteSettings" + ADD COLUMN IF NOT EXISTS "RiskRulesJson" text NOT NULL DEFAULT '{RiskRuleSettings.Serialize(RiskRuleSettings.Defaults).Replace("'", "''")}'; + + UPDATE "SiteSettings" + SET "RiskRulesJson" = '{RiskRuleSettings.Serialize(RiskRuleSettings.Defaults).Replace("'", "''")}' + WHERE "RiskRulesJson" IS NULL OR btrim("RiskRulesJson") = '' OR "RiskRulesJson" = '[]'; + """); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql( + """ + ALTER TABLE IF EXISTS "SiteSettings" + DROP COLUMN IF EXISTS "RiskRulesJson"; + """); + } + } +} diff --git a/Backend/Migrations/20260625110000_AddFooterPageContent.cs b/Backend/Migrations/20260625110000_AddFooterPageContent.cs new file mode 100644 index 0000000..edb44cc --- /dev/null +++ b/Backend/Migrations/20260625110000_AddFooterPageContent.cs @@ -0,0 +1,54 @@ +using Backend.Data; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backend.Migrations +{ + /// + [DbContext(typeof(AwardsDbContext))] + [Migration("20260625110000_AddFooterPageContent")] + public partial class AddFooterPageContent : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + var imprintContent = SeedCatalog.DefaultImprintContent.Replace("'", "''"); + var contactContent = SeedCatalog.DefaultContactContent.Replace("'", "''"); + var sponsorsContent = SeedCatalog.DefaultSponsorsContent.Replace("'", "''"); + + migrationBuilder.Sql( + $""" + ALTER TABLE IF EXISTS "SiteSettings" + ADD COLUMN IF NOT EXISTS "ImprintContent" text NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS "ContactContent" text NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS "SponsorsContent" text NOT NULL DEFAULT ''; + + UPDATE "SiteSettings" + SET "ImprintContent" = '{imprintContent}' + WHERE "ImprintContent" IS NULL OR btrim("ImprintContent") = ''; + + UPDATE "SiteSettings" + SET "ContactContent" = '{contactContent}' + WHERE "ContactContent" IS NULL OR btrim("ContactContent") = ''; + + UPDATE "SiteSettings" + SET "SponsorsContent" = '{sponsorsContent}' + WHERE "SponsorsContent" IS NULL OR btrim("SponsorsContent") = ''; + """); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql( + """ + ALTER TABLE IF EXISTS "SiteSettings" + DROP COLUMN IF EXISTS "ImprintContent", + DROP COLUMN IF EXISTS "ContactContent", + DROP COLUMN IF EXISTS "SponsorsContent"; + """); + } + } +} diff --git a/Backend/Migrations/20260625140614_AddTeamManagement.Designer.cs b/Backend/Migrations/20260625140614_AddTeamManagement.Designer.cs new file mode 100644 index 0000000..9fe9826 --- /dev/null +++ b/Backend/Migrations/20260625140614_AddTeamManagement.Designer.cs @@ -0,0 +1,1428 @@ +// +using System; +using Backend.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backend.Migrations +{ + [DbContext(typeof(AwardsDbContext))] + [Migration("20260625140614_AddTeamManagement")] + partial class AddTeamManagement + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backend.Domain.AdminAuditEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("AdminTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.ToTable("AdminAuditEntries"); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CategoryName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "CategoryId") + .IsUnique(); + + b.ToTable("Results"); + + b.HasData( + new + { + Id = 1, + CandidateId = 8, + CategoryId = 5, + CategoryName = "VTuber des Jahres", + SeasonId = 2 + }, + new + { + Id = 2, + CandidateId = 9, + CategoryId = 6, + CategoryName = "Bestes Live Event", + SeasonId = 2 + }, + new + { + Id = 3, + CandidateId = 10, + CategoryId = 7, + CategoryName = "Clip des Jahres", + SeasonId = 2 + }, + new + { + Id = 4, + CandidateId = 11, + CategoryId = 8, + CategoryName = "VTuber des Jahres", + SeasonId = 3 + }, + new + { + Id = 5, + CandidateId = 12, + CategoryId = 9, + CategoryName = "Clip des Jahres", + SeasonId = 3 + }, + new + { + Id = 6, + CandidateId = 13, + CategoryId = 10, + CategoryName = "VTuber des Jahres", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ChannelSlug") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId"); + + b.ToTable("Candidates"); + + b.HasData( + new + { + Id = 1, + CategoryId = 1, + ChannelSlug = "@hoshimimiyu", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 2, + CategoryId = 1, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 3, + CategoryId = 1, + ChannelSlug = "@shiroch", + DisplayName = "Shiro Ch.", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 4, + CategoryId = 2, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 5, + CategoryId = 2, + ChannelSlug = "@aoisakura", + DisplayName = "Aoi Sakura Showcase", + Platform = "YouTube", + SeasonId = 1 + }, + new + { + Id = 6, + CategoryId = 3, + ChannelSlug = "@pyonkichikingdom", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 7, + CategoryId = 4, + ChannelSlug = "@moonrelay", + DisplayName = "Moonrelay", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 8, + CategoryId = 5, + ChannelSlug = "@hoshimimiyu", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 9, + CategoryId = 6, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 10, + CategoryId = 7, + ChannelSlug = "@pyonkichikingdom", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 11, + CategoryId = 8, + ChannelSlug = "@aoisakura", + DisplayName = "Aoi Sakura", + Platform = "YouTube", + SeasonId = 3 + }, + new + { + Id = 12, + CategoryId = 9, + ChannelSlug = "@starbyte", + DisplayName = "Starbyte", + Platform = "Twitch", + SeasonId = 3 + }, + new + { + Id = 13, + CategoryId = 10, + ChannelSlug = "@tenshivox", + DisplayName = "Tenshi Vox", + Platform = "Twitch", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MaxNomineesPerUser") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Slug") + .IsUnique(); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = 1, + Description = "Die groesste Auszeichnung des Jahres.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 1, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 2, + Description = "Events, Konzerte und 3D-Shows.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 1, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 3, + Description = "Der lustigste oder emotionalste Clip des Jahres.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 1, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 4, + Description = "Die aktivste und freundlichste Community.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "Beste Community", + SeasonId = 1, + Slug = "beste-community", + SortOrder = 4 + }, + new + { + Id = 5, + Description = "Archivkategorie 2025.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 2, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 6, + Description = "Archivkategorie 2025.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 2, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 7, + Description = "Archivkategorie 2025.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 2, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 8, + Description = "Archivkategorie 2024.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 3, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 9, + Description = "Archivkategorie 2024.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 3, + Slug = "clip-des-jahres", + SortOrder = 2 + }, + new + { + Id = 10, + Description = "Archivkategorie 2023.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 4, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ClipUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Creator") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("ClipSubmissions"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CandidateText") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("StreamUrl") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("Nominations"); + + b.HasData( + new + { + Id = 1, + CandidateText = "Hoshimi Miyu", + CategoryId = 1, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 13, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_hoshi" + }, + new + { + Id = 2, + CandidateText = "Kurainu 3D Live", + CategoryId = 2, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 14, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_kurainu" + }); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("TwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("RiskFlags"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrentPhase") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("IsCommunityOnly") + .HasColumnType("boolean"); + + b.Property("IsCurrent") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("NominationEndsAt") + .HasColumnType("date"); + + b.Property("NominationStartsAt") + .HasColumnType("date"); + + b.Property("ReviewEndsAt") + .HasColumnType("date"); + + b.Property("ReviewStartsAt") + .HasColumnType("date"); + + b.Property("ShowDate") + .HasColumnType("date"); + + b.Property("ShowStartsAt") + .HasColumnType("time without time zone"); + + b.Property("ShowStreamUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("VotingEndsAt") + .HasColumnType("date"); + + b.Property("VotingStartsAt") + .HasColumnType("date"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("Seasons"); + + b.HasData( + new + { + Id = 1, + CurrentPhase = "Community Voting", + IsCommunityOnly = true, + IsCurrent = true, + Name = "VTuber Star Awards 2026", + NominationEndsAt = new DateOnly(2026, 5, 31), + NominationStartsAt = new DateOnly(2026, 5, 1), + ReviewEndsAt = new DateOnly(2026, 7, 10), + ReviewStartsAt = new DateOnly(2026, 7, 1), + ShowDate = new DateOnly(2026, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2026, 6, 30), + VotingStartsAt = new DateOnly(2026, 6, 1), + Year = 2026 + }, + new + { + Id = 2, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2025", + NominationEndsAt = new DateOnly(2025, 5, 31), + NominationStartsAt = new DateOnly(2025, 5, 1), + ReviewEndsAt = new DateOnly(2025, 7, 10), + ReviewStartsAt = new DateOnly(2025, 7, 1), + ShowDate = new DateOnly(2025, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2025, 6, 30), + VotingStartsAt = new DateOnly(2025, 6, 1), + Year = 2025 + }, + new + { + Id = 3, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2024", + NominationEndsAt = new DateOnly(2024, 5, 31), + NominationStartsAt = new DateOnly(2024, 5, 1), + ReviewEndsAt = new DateOnly(2024, 7, 10), + ReviewStartsAt = new DateOnly(2024, 7, 1), + ShowDate = new DateOnly(2024, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://youtube.com/c/Jayuhime", + VotingEndsAt = new DateOnly(2024, 6, 30), + VotingStartsAt = new DateOnly(2024, 6, 1), + Year = 2024 + }, + new + { + Id = 4, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2023", + NominationEndsAt = new DateOnly(2023, 5, 31), + NominationStartsAt = new DateOnly(2023, 5, 1), + ReviewEndsAt = new DateOnly(2023, 7, 10), + ReviewStartsAt = new DateOnly(2023, 7, 1), + ShowDate = new DateOnly(2023, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2023, 6, 30), + VotingStartsAt = new DateOnly(2023, 6, 1), + Year = 2023 + }); + }); + + modelBuilder.Entity("Backend.Domain.SiteSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContactContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("DemoLoginDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginEmail") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("DemoLoginEnabled") + .HasColumnType("boolean"); + + b.Property("DemoLoginManagedByDatabase") + .HasColumnType("boolean"); + + b.Property("DemoLoginPasswordHash") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginPasswordSalt") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DemoLoginTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("FaqJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("HostDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("HostTagline") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("ImprintContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("ImprintUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("MaintenanceMessage") + .IsRequired() + .HasMaxLength(600) + .HasColumnType("character varying(600)"); + + b.Property("MaintenanceModeEnabled") + .HasColumnType("boolean"); + + b.Property("MaintenanceTitle") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("NewsletterUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("PrivacyEmail") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("PrivacyPolicyContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("PrivacyPolicyUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivacyPolicyUpdatedBy") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("RiskRulesJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SocialLinksJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.ToTable("SiteSettings"); + + b.HasData( + new + { + Id = 1, + ContactContent = "Kontakt zum Award-Team\nDu hast Fragen zur Nominierung, zum Voting, zu Clips oder zur Show? Schreib dem Team ueber die hinterlegte Kontaktseite.\n\nDatenschutzfragen\nFuer Datenschutzanfragen nutze bitte die Datenschutz-E-Mail aus diesem Footer.\n\nCommunity & Kooperationen\nSocial Links und Partneranfragen werden vom Admin-Team gepflegt und koordiniert.", + ContactUrl = "https://vtuber-star-awards.de/kontakt", + DemoLoginDisplayName = "Jayuhime Admin", + DemoLoginEmail = "", + DemoLoginEnabled = false, + DemoLoginManagedByDatabase = false, + DemoLoginPasswordHash = "", + DemoLoginPasswordSalt = "", + DemoLoginTwitchUserId = "jayuhime_admin", + FaqJson = "[{\"question\":\"Wer darf nominiert werden?\",\"answer\":\"Jede:r aktive deutschsprachige VTuber kann nominiert werden \\u2014 unabh\\u00E4ngig von Follower-Zahl oder Plattform. Die Community schl\\u00E4gt in der Nominierungsphase ihre Favorit:innen vor.\"},{\"question\":\"Wie funktioniert das Voting?\",\"answer\":\"Du meldest dich ausschlie\\u00DFlich mit deinem Twitch-Account an \\u2014 nur so kannst du teilnehmen. Das h\\u00E4lt das Voting fair und bot-frei. Danach vergibst du pro Kategorie eine Stimme, \\u00E4nderbar bis zum Ende der Phase.\"},{\"question\":\"Was kostet die Teilnahme?\",\"answer\":\"Gar nichts. Nominieren, voten und die Show schauen ist komplett kostenlos \\u2014 der VTuber Star Award ist ein Community-Event von Fans f\\u00FCr Fans.\"},{\"question\":\"Wann und wo findet die Award-Show statt?\",\"answer\":\"Die gro\\u00DFe Live-Show wird von Jayuhime gehostet und auf Twitch \\u0026 YouTube gestreamt. Den genauen Termin findest du im Countdown oben \\u2014 sei live dabei, wenn die Stars gek\\u00FCrt werden!\"},{\"question\":\"Ich wurde nominiert \\u2014 was nun?\",\"answer\":\"Gl\\u00FCckwunsch! Du erh\\u00E4ltst eine Benachrichtigung mit allen Infos. Teile deine Nominierung gern mit deiner Community und ruf zum Voten auf \\u2014 jede Stimme z\\u00E4hlt.\"}]", + HostDisplayName = "Jayuhime", + HostTagline = "VTuber & Award Host", + ImprintContent = "Anbieter\nVTuber Star Awards, vertreten durch Jayuhime.\n\nKontakt\nNutze fuer organisatorische Fragen bitte die Kontaktseite oder die hinterlegte Kontaktadresse.\n\nHinweis\nDieses Impressum ist ein redaktioneller Platzhalter und sollte vor dem Livegang mit den tatsaechlichen Anbieterangaben ersetzt werden.", + ImprintUrl = "https://vtuber-star-awards.de/impressum", + MaintenanceMessage = "Die Award-Galaxie wird gerade liebevoll poliert. Schau gleich wieder vorbei.", + MaintenanceModeEnabled = false, + MaintenanceTitle = "Sternenpause", + NewsletterUrl = "https://vtuber-star-awards.de/newsletter", + PrivacyEmail = "datenschutz@vtuber-star-awards.de", + PrivacyPolicyContent = "Verantwortliche:r\nVTuber Star Awards, vertreten durch Jayuhime. Kontakt: datenschutz@vtuber-star-awards.de\n\nWelche Daten wir verarbeiten\nBei Teilnahme (Voting, Nominierung, Clip-Einreichung) verarbeiten wir ausschließlich deine Twitch-User-ID sowie den Zeitstempel deiner Aktion.\nFür Show-Erinnerungen speichern wir optional deine E-Mail-Adresse.\n\nRechtsgrundlage\nVerarbeitung auf Basis von Art. 6 Abs. 1 lit. b DSGVO. Für optionale Erinnerungen gilt Art. 6 Abs. 1 lit. a DSGVO.\n\nZweck der Verarbeitung\nDurchführung des VTuber Star Awards, Sicherstellung fairer Abstimmung, Spam-Prävention sowie Admin-Review von Einreichungen.\n\nLöschfristen\nAlle Teilnahmedaten werden spätestens 6 Monate nach der Award-Show automatisch gelöscht. E-Mail-Adressen werden nach der Show entfernt.\n\nDeine Rechte\nDu hast das Recht auf Auskunft, Löschung, Berichtigung, Widerspruch und Beschwerde bei der zuständigen Aufsichtsbehörde.\n\nWeitergabe an Dritte\nKeine Weitergabe zu Werbezwecken. Technische Dienstleister verarbeiten Daten ausschließlich im Rahmen der Auftragsverarbeitung.", + PrivacyPolicyUpdatedAt = new DateTimeOffset(new DateTime(2026, 6, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + PrivacyPolicyUpdatedBy = "seed", + RiskRulesJson = "[{\"key\":\"resubmitted_ballot\",\"label\":\"Ballot erneut gespeichert\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"low\",\"description\":\"Erstellt einen Hinweis, wenn ein User sein Voting erneut speichert.\"},{\"key\":\"rapid_vote_updates\",\"label\":\"Voting-Burst\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr schnell mehrere Voting-Aenderungen ausloest.\"},{\"key\":\"resubmitted_nomination\",\"label\":\"Nominierung erneut eingereicht\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"low\",\"description\":\"Erstellt einen Hinweis, wenn ein User in derselben Kategorie erneut nominiert.\"},{\"key\":\"rapid_nomination_burst\",\"label\":\"Nominierungs-Burst\",\"enabled\":true,\"threshold\":10,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr viele Nominierungen in kurzer Zeit sendet.\"},{\"key\":\"duplicate_clip_submission\",\"label\":\"Doppelter Clip\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn ein User denselben Clip erneut einreicht.\"},{\"key\":\"rapid_clip_burst\",\"label\":\"Clip-Burst\",\"enabled\":true,\"threshold\":5,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr viele Clips in kurzer Zeit sendet.\"},{\"key\":\"rapid_login_ip\",\"label\":\"Login-Burst pro IP\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":15,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn mehrere neue Sessions von derselben IP entstehen.\"},{\"key\":\"rapid_demo_login_ip\",\"label\":\"Demo-Login-Burst pro IP\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":15,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn mehrere Demo-Admin-Sessions von derselben IP entstehen.\"}]", + SocialLinksJson = "[{\"label\":\"Twitch\",\"platform\":\"twitch\",\"url\":\"https://twitch.tv/jayuhime\",\"icon\":\"twitch\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"YouTube\",\"platform\":\"youtube\",\"url\":\"https://youtube.com/c/Jayuhime\",\"icon\":\"youtube\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"X\",\"platform\":\"x\",\"url\":\"https://x.com/jayuhime\",\"icon\":\"x\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Instagram\",\"platform\":\"instagram\",\"url\":\"https://instagram.com/jayuhime\",\"icon\":\"instagram\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Discord\",\"platform\":\"discord\",\"url\":\"https://discord.gg/jayuhime\",\"icon\":\"discord\",\"showOnHost\":true,\"showOnCommunity\":true}]", + SponsorsContent = "Sponsoren & Partner\nHier koennen Sponsor:innen, Medienpartner, Community-Partner und Supporter des VTuber Star Awards vorgestellt werden.\n\nPartner werden im Rahmen der Show und auf den oeffentlichen Kontaktflaechen genannt, sobald sie final bestaetigt sind.", + SponsorsUrl = "https://vtuber-star-awards.de/partner" + }); + }); + + modelBuilder.Entity("Backend.Domain.TeamMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Login") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MustChangePassword") + .HasColumnType("boolean"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("PasswordResetAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("Login") + .IsUnique(); + + b.ToTable("TeamMembers"); + }); + + modelBuilder.Entity("Backend.Domain.TeamRolePermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PermissionsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("Role") + .IsUnique(); + + b.ToTable("TeamRolePermissions"); + }); + + modelBuilder.Entity("Backend.Domain.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SessionToken") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SessionToken") + .IsUnique(); + + b.ToTable("UserSessions"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("VoteBallots"); + + b.HasData( + new + { + Id = 1, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_1" + }, + new + { + Id = 2, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 5, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_2" + }); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BallotId") + .HasColumnType("integer"); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BallotId"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.ToTable("VoteEntries"); + + b.HasData( + new + { + Id = 1, + BallotId = 1, + CandidateId = 1, + CategoryId = 1 + }, + new + { + Id = 2, + BallotId = 1, + CandidateId = 4, + CategoryId = 2 + }, + new + { + Id = 3, + BallotId = 2, + CandidateId = 2, + CategoryId = 1 + }, + new + { + Id = 4, + BallotId = 2, + CandidateId = 6, + CategoryId = 3 + }); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Results") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.HasOne("Backend.Domain.Category", "Category") + .WithMany("Candidates") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Categories") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Candidate"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId"); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.HasOne("Backend.Domain.VoteBallot", "Ballot") + .WithMany("Entries") + .HasForeignKey("BallotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ballot"); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Navigation("Candidates"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Navigation("Categories"); + + b.Navigation("Results"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Navigation("Entries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/Migrations/20260625140614_AddTeamManagement.cs b/Backend/Migrations/20260625140614_AddTeamManagement.cs new file mode 100644 index 0000000..64d85d3 --- /dev/null +++ b/Backend/Migrations/20260625140614_AddTeamManagement.cs @@ -0,0 +1,79 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backend.Migrations +{ + /// + public partial class AddTeamManagement : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "TeamMembers", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Login = table.Column(type: "character varying(80)", maxLength: 80, nullable: false), + DisplayName = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), + Role = table.Column(type: "character varying(40)", maxLength: 40, nullable: false), + PasswordHash = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), + PasswordSalt = table.Column(type: "character varying(80)", maxLength: 80, nullable: false), + MustChangePassword = table.Column(type: "boolean", nullable: false), + IsActive = table.Column(type: "boolean", nullable: false), + CreatedByTwitchId = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), + UpdatedByTwitchId = table.Column(type: "character varying(120)", maxLength: 120, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + LastLoginAt = table.Column(type: "timestamp with time zone", nullable: true), + PasswordResetAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_TeamMembers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "TeamRolePermissions", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Role = table.Column(type: "character varying(40)", maxLength: 40, nullable: false), + PermissionsJson = table.Column(type: "text", nullable: false), + UpdatedByTwitchId = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TeamRolePermissions", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_TeamMembers_Login", + table: "TeamMembers", + column: "Login", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_TeamRolePermissions_Role", + table: "TeamRolePermissions", + column: "Role", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TeamMembers"); + + migrationBuilder.DropTable( + name: "TeamRolePermissions"); + } + } +} diff --git a/Backend/Migrations/20260625153000_AddCreatorRoleAndTeamTwitchBinding.cs b/Backend/Migrations/20260625153000_AddCreatorRoleAndTeamTwitchBinding.cs new file mode 100644 index 0000000..4ce10c1 --- /dev/null +++ b/Backend/Migrations/20260625153000_AddCreatorRoleAndTeamTwitchBinding.cs @@ -0,0 +1,66 @@ +using System; +using Backend.Data; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backend.Migrations +{ + /// + [DbContext(typeof(AwardsDbContext))] + [Migration("20260625153000_AddCreatorRoleAndTeamTwitchBinding")] + public partial class AddCreatorRoleAndTeamTwitchBinding : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "BoundTwitchDisplayName", + table: "TeamMembers", + type: "character varying(120)", + maxLength: 120, + nullable: true); + + migrationBuilder.AddColumn( + name: "BoundTwitchUserId", + table: "TeamMembers", + type: "character varying(120)", + maxLength: 120, + nullable: true); + + migrationBuilder.AddColumn( + name: "TwitchBoundAt", + table: "TeamMembers", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_TeamMembers_BoundTwitchUserId", + table: "TeamMembers", + column: "BoundTwitchUserId", + unique: true, + filter: "\"BoundTwitchUserId\" IS NOT NULL"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_TeamMembers_BoundTwitchUserId", + table: "TeamMembers"); + + migrationBuilder.DropColumn( + name: "BoundTwitchDisplayName", + table: "TeamMembers"); + + migrationBuilder.DropColumn( + name: "BoundTwitchUserId", + table: "TeamMembers"); + + migrationBuilder.DropColumn( + name: "TwitchBoundAt", + table: "TeamMembers"); + } + } +} diff --git a/Backend/Migrations/20260626113057_AddVoteBallotSubmitterUniqueIndex.Designer.cs b/Backend/Migrations/20260626113057_AddVoteBallotSubmitterUniqueIndex.Designer.cs new file mode 100644 index 0000000..385541c --- /dev/null +++ b/Backend/Migrations/20260626113057_AddVoteBallotSubmitterUniqueIndex.Designer.cs @@ -0,0 +1,1471 @@ +// +using System; +using Backend.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backend.Migrations +{ + [DbContext(typeof(AwardsDbContext))] + [Migration("20260626113057_AddVoteBallotSubmitterUniqueIndex")] + partial class AddVoteBallotSubmitterUniqueIndex + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backend.Domain.AdminAuditEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("AdminTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.ToTable("AdminAuditEntries"); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CategoryName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "CategoryId") + .IsUnique(); + + b.ToTable("Results"); + + b.HasData( + new + { + Id = 1, + CandidateId = 8, + CategoryId = 5, + CategoryName = "VTuber des Jahres", + SeasonId = 2 + }, + new + { + Id = 2, + CandidateId = 9, + CategoryId = 6, + CategoryName = "Bestes Live Event", + SeasonId = 2 + }, + new + { + Id = 3, + CandidateId = 10, + CategoryId = 7, + CategoryName = "Clip des Jahres", + SeasonId = 2 + }, + new + { + Id = 4, + CandidateId = 11, + CategoryId = 8, + CategoryName = "VTuber des Jahres", + SeasonId = 3 + }, + new + { + Id = 5, + CandidateId = 12, + CategoryId = 9, + CategoryName = "Clip des Jahres", + SeasonId = 3 + }, + new + { + Id = 6, + CandidateId = 13, + CategoryId = 10, + CategoryName = "VTuber des Jahres", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ChannelSlug") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId"); + + b.ToTable("Candidates"); + + b.HasData( + new + { + Id = 1, + CategoryId = 1, + ChannelSlug = "@hoshimimiyu", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 2, + CategoryId = 1, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 3, + CategoryId = 1, + ChannelSlug = "@shiroch", + DisplayName = "Shiro Ch.", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 4, + CategoryId = 2, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 5, + CategoryId = 2, + ChannelSlug = "@aoisakura", + DisplayName = "Aoi Sakura Showcase", + Platform = "YouTube", + SeasonId = 1 + }, + new + { + Id = 6, + CategoryId = 3, + ChannelSlug = "@pyonkichikingdom", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 7, + CategoryId = 4, + ChannelSlug = "@moonrelay", + DisplayName = "Moonrelay", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 8, + CategoryId = 5, + ChannelSlug = "@hoshimimiyu", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 9, + CategoryId = 6, + ChannelSlug = "@kurainu", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 10, + CategoryId = 7, + ChannelSlug = "@pyonkichikingdom", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 11, + CategoryId = 8, + ChannelSlug = "@aoisakura", + DisplayName = "Aoi Sakura", + Platform = "YouTube", + SeasonId = 3 + }, + new + { + Id = 12, + CategoryId = 9, + ChannelSlug = "@starbyte", + DisplayName = "Starbyte", + Platform = "Twitch", + SeasonId = 3 + }, + new + { + Id = 13, + CategoryId = 10, + ChannelSlug = "@tenshivox", + DisplayName = "Tenshi Vox", + Platform = "Twitch", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MaxNomineesPerUser") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Slug") + .IsUnique(); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = 1, + Description = "Die groesste Auszeichnung des Jahres.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 1, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 2, + Description = "Events, Konzerte und 3D-Shows.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 1, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 3, + Description = "Der lustigste oder emotionalste Clip des Jahres.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 1, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 4, + Description = "Die aktivste und freundlichste Community.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "Beste Community", + SeasonId = 1, + Slug = "beste-community", + SortOrder = 4 + }, + new + { + Id = 5, + Description = "Archivkategorie 2025.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 2, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 6, + Description = "Archivkategorie 2025.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 2, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 7, + Description = "Archivkategorie 2025.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 2, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 8, + Description = "Archivkategorie 2024.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 3, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 9, + Description = "Archivkategorie 2024.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 3, + Slug = "clip-des-jahres", + SortOrder = 2 + }, + new + { + Id = 10, + Description = "Archivkategorie 2023.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 4, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ClipUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Creator") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("ClipSubmissions"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CandidateText") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("StreamUrl") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("Nominations"); + + b.HasData( + new + { + Id = 1, + CandidateText = "Hoshimi Miyu", + CategoryId = 1, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 13, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_hoshi" + }, + new + { + Id = 2, + CandidateText = "Kurainu 3D Live", + CategoryId = 2, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 14, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_kurainu" + }); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("TwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("RiskFlags"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrentPhase") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("IsCommunityOnly") + .HasColumnType("boolean"); + + b.Property("IsCurrent") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("NominationEndsAt") + .HasColumnType("date"); + + b.Property("NominationStartsAt") + .HasColumnType("date"); + + b.Property("ReviewEndsAt") + .HasColumnType("date"); + + b.Property("ReviewStartsAt") + .HasColumnType("date"); + + b.Property("ShowDate") + .HasColumnType("date"); + + b.Property("ShowStartsAt") + .HasColumnType("time without time zone"); + + b.Property("ShowStreamUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("VotingEndsAt") + .HasColumnType("date"); + + b.Property("VotingStartsAt") + .HasColumnType("date"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("Seasons"); + + b.HasData( + new + { + Id = 1, + CurrentPhase = "Community Voting", + IsCommunityOnly = true, + IsCurrent = true, + Name = "VTuber Star Awards 2026", + NominationEndsAt = new DateOnly(2026, 5, 31), + NominationStartsAt = new DateOnly(2026, 5, 1), + ReviewEndsAt = new DateOnly(2026, 7, 10), + ReviewStartsAt = new DateOnly(2026, 7, 1), + ShowDate = new DateOnly(2026, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2026, 6, 30), + VotingStartsAt = new DateOnly(2026, 6, 1), + Year = 2026 + }, + new + { + Id = 2, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2025", + NominationEndsAt = new DateOnly(2025, 5, 31), + NominationStartsAt = new DateOnly(2025, 5, 1), + ReviewEndsAt = new DateOnly(2025, 7, 10), + ReviewStartsAt = new DateOnly(2025, 7, 1), + ShowDate = new DateOnly(2025, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2025, 6, 30), + VotingStartsAt = new DateOnly(2025, 6, 1), + Year = 2025 + }, + new + { + Id = 3, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2024", + NominationEndsAt = new DateOnly(2024, 5, 31), + NominationStartsAt = new DateOnly(2024, 5, 1), + ReviewEndsAt = new DateOnly(2024, 7, 10), + ReviewStartsAt = new DateOnly(2024, 7, 1), + ShowDate = new DateOnly(2024, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://youtube.com/c/Jayuhime", + VotingEndsAt = new DateOnly(2024, 6, 30), + VotingStartsAt = new DateOnly(2024, 6, 1), + Year = 2024 + }, + new + { + Id = 4, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2023", + NominationEndsAt = new DateOnly(2023, 5, 31), + NominationStartsAt = new DateOnly(2023, 5, 1), + ReviewEndsAt = new DateOnly(2023, 7, 10), + ReviewStartsAt = new DateOnly(2023, 7, 1), + ShowDate = new DateOnly(2023, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2023, 6, 30), + VotingStartsAt = new DateOnly(2023, 6, 1), + Year = 2023 + }); + }); + + modelBuilder.Entity("Backend.Domain.SiteSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContactContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("DemoLoginDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginEmail") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("DemoLoginEnabled") + .HasColumnType("boolean"); + + b.Property("DemoLoginManagedByDatabase") + .HasColumnType("boolean"); + + b.Property("DemoLoginPasswordHash") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginPasswordSalt") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DemoLoginTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("FaqJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("HostDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("HostTagline") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("ImprintContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("ImprintUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("MaintenanceMessage") + .IsRequired() + .HasMaxLength(600) + .HasColumnType("character varying(600)"); + + b.Property("MaintenanceModeEnabled") + .HasColumnType("boolean"); + + b.Property("MaintenanceTitle") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("NewsletterUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("PrivacyEmail") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("PrivacyPolicyContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("PrivacyPolicyUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivacyPolicyUpdatedBy") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("RiskRulesJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SocialLinksJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("TwitchAuthManagedByDatabase") + .HasColumnType("boolean"); + + b.Property("TwitchClientId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchClientSecret") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("TwitchRedirectUri") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("TwitchScope") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.HasKey("Id"); + + b.ToTable("SiteSettings"); + + b.HasData( + new + { + Id = 1, + ContactContent = "Kontakt zum Award-Team\nDu hast Fragen zur Nominierung, zum Voting, zu Clips oder zur Show? Schreib dem Team ueber die hinterlegte Kontaktseite.\n\nDatenschutzfragen\nFuer Datenschutzanfragen nutze bitte die Datenschutz-E-Mail aus diesem Footer.\n\nCommunity & Kooperationen\nSocial Links und Partneranfragen werden vom Admin-Team gepflegt und koordiniert.", + ContactUrl = "https://vtuber-star-awards.de/kontakt", + DemoLoginDisplayName = "Jayuhime Admin", + DemoLoginEmail = "", + DemoLoginEnabled = false, + DemoLoginManagedByDatabase = false, + DemoLoginPasswordHash = "", + DemoLoginPasswordSalt = "", + DemoLoginTwitchUserId = "jayuhime_admin", + FaqJson = "[{\"question\":\"Wer darf nominiert werden?\",\"answer\":\"Jede:r aktive deutschsprachige VTuber kann nominiert werden \\u2014 unabh\\u00E4ngig von Follower-Zahl oder Plattform. Die Community schl\\u00E4gt in der Nominierungsphase ihre Favorit:innen vor.\"},{\"question\":\"Wie funktioniert das Voting?\",\"answer\":\"Du meldest dich ausschlie\\u00DFlich mit deinem Twitch-Account an \\u2014 nur so kannst du teilnehmen. Das h\\u00E4lt das Voting fair und bot-frei. Danach vergibst du pro Kategorie eine Stimme, \\u00E4nderbar bis zum Ende der Phase.\"},{\"question\":\"Was kostet die Teilnahme?\",\"answer\":\"Gar nichts. Nominieren, voten und die Show schauen ist komplett kostenlos \\u2014 der VTuber Star Award ist ein Community-Event von Fans f\\u00FCr Fans.\"},{\"question\":\"Wann und wo findet die Award-Show statt?\",\"answer\":\"Die gro\\u00DFe Live-Show wird von Jayuhime gehostet und auf Twitch \\u0026 YouTube gestreamt. Den genauen Termin findest du im Countdown oben \\u2014 sei live dabei, wenn die Stars gek\\u00FCrt werden!\"},{\"question\":\"Ich wurde nominiert \\u2014 was nun?\",\"answer\":\"Gl\\u00FCckwunsch! Du erh\\u00E4ltst eine Benachrichtigung mit allen Infos. Teile deine Nominierung gern mit deiner Community und ruf zum Voten auf \\u2014 jede Stimme z\\u00E4hlt.\"}]", + HostDisplayName = "Jayuhime", + HostTagline = "VTuber & Award Host", + ImprintContent = "Anbieter\nVTuber Star Awards, vertreten durch Jayuhime.\n\nKontakt\nNutze fuer organisatorische Fragen bitte die Kontaktseite oder die hinterlegte Kontaktadresse.\n\nHinweis\nDieses Impressum ist ein redaktioneller Platzhalter und sollte vor dem Livegang mit den tatsaechlichen Anbieterangaben ersetzt werden.", + ImprintUrl = "https://vtuber-star-awards.de/impressum", + MaintenanceMessage = "Die Award-Galaxie wird gerade liebevoll poliert. Schau gleich wieder vorbei.", + MaintenanceModeEnabled = false, + MaintenanceTitle = "Sternenpause", + NewsletterUrl = "https://vtuber-star-awards.de/newsletter", + PrivacyEmail = "datenschutz@vtuber-star-awards.de", + PrivacyPolicyContent = "Verantwortliche:r\nVTuber Star Awards, vertreten durch Jayuhime. Kontakt: datenschutz@vtuber-star-awards.de\n\nWelche Daten wir verarbeiten\nBei Teilnahme (Voting, Nominierung, Clip-Einreichung) verarbeiten wir ausschließlich deine Twitch-User-ID sowie den Zeitstempel deiner Aktion.\nFür Show-Erinnerungen speichern wir optional deine E-Mail-Adresse.\n\nRechtsgrundlage\nVerarbeitung auf Basis von Art. 6 Abs. 1 lit. b DSGVO. Für optionale Erinnerungen gilt Art. 6 Abs. 1 lit. a DSGVO.\n\nZweck der Verarbeitung\nDurchführung des VTuber Star Awards, Sicherstellung fairer Abstimmung, Spam-Prävention sowie Admin-Review von Einreichungen.\n\nLöschfristen\nAlle Teilnahmedaten werden spätestens 6 Monate nach der Award-Show automatisch gelöscht. E-Mail-Adressen werden nach der Show entfernt.\n\nDeine Rechte\nDu hast das Recht auf Auskunft, Löschung, Berichtigung, Widerspruch und Beschwerde bei der zuständigen Aufsichtsbehörde.\n\nWeitergabe an Dritte\nKeine Weitergabe zu Werbezwecken. Technische Dienstleister verarbeiten Daten ausschließlich im Rahmen der Auftragsverarbeitung.", + PrivacyPolicyUpdatedAt = new DateTimeOffset(new DateTime(2026, 6, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + PrivacyPolicyUpdatedBy = "seed", + RiskRulesJson = "[{\"key\":\"resubmitted_ballot\",\"label\":\"Ballot erneut gespeichert\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"low\",\"description\":\"Erstellt einen Hinweis, wenn ein User sein Voting erneut speichert.\"},{\"key\":\"rapid_vote_updates\",\"label\":\"Voting-Burst\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr schnell mehrere Voting-Aenderungen ausloest.\"},{\"key\":\"resubmitted_nomination\",\"label\":\"Nominierung erneut eingereicht\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"low\",\"description\":\"Erstellt einen Hinweis, wenn ein User in derselben Kategorie erneut nominiert.\"},{\"key\":\"rapid_nomination_burst\",\"label\":\"Nominierungs-Burst\",\"enabled\":true,\"threshold\":10,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr viele Nominierungen in kurzer Zeit sendet.\"},{\"key\":\"duplicate_clip_submission\",\"label\":\"Doppelter Clip\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn ein User denselben Clip erneut einreicht.\"},{\"key\":\"rapid_clip_burst\",\"label\":\"Clip-Burst\",\"enabled\":true,\"threshold\":5,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr viele Clips in kurzer Zeit sendet.\"},{\"key\":\"rapid_login_ip\",\"label\":\"Login-Burst pro IP\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":15,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn mehrere neue Sessions von derselben IP entstehen.\"},{\"key\":\"rapid_demo_login_ip\",\"label\":\"Demo-Login-Burst pro IP\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":15,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn mehrere Demo-Admin-Sessions von derselben IP entstehen.\"}]", + SocialLinksJson = "[{\"label\":\"Twitch\",\"platform\":\"twitch\",\"url\":\"https://twitch.tv/jayuhime\",\"icon\":\"twitch\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"YouTube\",\"platform\":\"youtube\",\"url\":\"https://youtube.com/c/Jayuhime\",\"icon\":\"youtube\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"X\",\"platform\":\"x\",\"url\":\"https://x.com/jayuhime\",\"icon\":\"x\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Instagram\",\"platform\":\"instagram\",\"url\":\"https://instagram.com/jayuhime\",\"icon\":\"instagram\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Discord\",\"platform\":\"discord\",\"url\":\"https://discord.gg/jayuhime\",\"icon\":\"discord\",\"showOnHost\":true,\"showOnCommunity\":true}]", + SponsorsContent = "Sponsoren & Partner\nHier koennen Sponsor:innen, Medienpartner, Community-Partner und Supporter des VTuber Star Awards vorgestellt werden.\n\nPartner werden im Rahmen der Show und auf den oeffentlichen Kontaktflaechen genannt, sobald sie final bestaetigt sind.", + SponsorsUrl = "https://vtuber-star-awards.de/partner", + TwitchAuthManagedByDatabase = false, + TwitchClientId = "", + TwitchClientSecret = "", + TwitchRedirectUri = "", + TwitchScope = "" + }); + }); + + modelBuilder.Entity("Backend.Domain.TeamMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoundTwitchDisplayName") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("BoundTwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Login") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MustChangePassword") + .HasColumnType("boolean"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("PasswordResetAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("TwitchBoundAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("BoundTwitchUserId") + .IsUnique(); + + b.HasIndex("Login") + .IsUnique(); + + b.ToTable("TeamMembers"); + }); + + modelBuilder.Entity("Backend.Domain.TeamRolePermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PermissionsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("Role") + .IsUnique(); + + b.ToTable("TeamRolePermissions"); + }); + + modelBuilder.Entity("Backend.Domain.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SessionToken") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SessionToken") + .IsUnique(); + + b.ToTable("UserSessions"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "SubmittedByTwitchId") + .IsUnique(); + + b.ToTable("VoteBallots"); + + b.HasData( + new + { + Id = 1, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_1" + }, + new + { + Id = 2, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 5, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_2" + }); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BallotId") + .HasColumnType("integer"); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BallotId"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.ToTable("VoteEntries"); + + b.HasData( + new + { + Id = 1, + BallotId = 1, + CandidateId = 1, + CategoryId = 1 + }, + new + { + Id = 2, + BallotId = 1, + CandidateId = 4, + CategoryId = 2 + }, + new + { + Id = 3, + BallotId = 2, + CandidateId = 2, + CategoryId = 1 + }, + new + { + Id = 4, + BallotId = 2, + CandidateId = 6, + CategoryId = 3 + }); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Results") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.HasOne("Backend.Domain.Category", "Category") + .WithMany("Candidates") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Categories") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Candidate"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId"); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.HasOne("Backend.Domain.VoteBallot", "Ballot") + .WithMany("Entries") + .HasForeignKey("BallotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ballot"); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Navigation("Candidates"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Navigation("Categories"); + + b.Navigation("Results"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Navigation("Entries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/Migrations/20260626113057_AddVoteBallotSubmitterUniqueIndex.cs b/Backend/Migrations/20260626113057_AddVoteBallotSubmitterUniqueIndex.cs new file mode 100644 index 0000000..5af14c2 --- /dev/null +++ b/Backend/Migrations/20260626113057_AddVoteBallotSubmitterUniqueIndex.cs @@ -0,0 +1,135 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backend.Migrations +{ + /// + public partial class AddVoteBallotSubmitterUniqueIndex : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_VoteBallots_SeasonId", + table: "VoteBallots"); + + migrationBuilder.AddColumn( + name: "TwitchAuthManagedByDatabase", + table: "SiteSettings", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "TwitchClientId", + table: "SiteSettings", + type: "character varying(120)", + maxLength: 120, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "TwitchClientSecret", + table: "SiteSettings", + type: "character varying(180)", + maxLength: 180, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "TwitchRedirectUri", + table: "SiteSettings", + type: "character varying(400)", + maxLength: 400, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "TwitchScope", + table: "SiteSettings", + type: "character varying(300)", + maxLength: 300, + nullable: false, + defaultValue: ""); + + migrationBuilder.UpdateData( + table: "SiteSettings", + keyColumn: "Id", + keyValue: 1, + columns: new[] { "TwitchAuthManagedByDatabase", "TwitchClientId", "TwitchClientSecret", "TwitchRedirectUri", "TwitchScope" }, + values: new object[] { false, "", "", "", "" }); + + migrationBuilder.Sql(""" + DELETE FROM "VoteEntries" + WHERE "BallotId" IN ( + SELECT "Id" + FROM ( + SELECT + "Id", + ROW_NUMBER() OVER ( + PARTITION BY "SeasonId", "SubmittedByTwitchId" + ORDER BY "SubmittedAt" DESC, "Id" DESC + ) AS duplicate_rank + FROM "VoteBallots" + ) ranked_ballots + WHERE duplicate_rank > 1 + ); + + DELETE FROM "VoteBallots" + WHERE "Id" IN ( + SELECT "Id" + FROM ( + SELECT + "Id", + ROW_NUMBER() OVER ( + PARTITION BY "SeasonId", "SubmittedByTwitchId" + ORDER BY "SubmittedAt" DESC, "Id" DESC + ) AS duplicate_rank + FROM "VoteBallots" + ) ranked_ballots + WHERE duplicate_rank > 1 + ); + """); + + migrationBuilder.CreateIndex( + name: "IX_VoteBallots_SeasonId_SubmittedByTwitchId", + table: "VoteBallots", + columns: new[] { "SeasonId", "SubmittedByTwitchId" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_VoteBallots_SeasonId_SubmittedByTwitchId", + table: "VoteBallots"); + + migrationBuilder.DropColumn( + name: "TwitchAuthManagedByDatabase", + table: "SiteSettings"); + + migrationBuilder.DropColumn( + name: "TwitchClientId", + table: "SiteSettings"); + + migrationBuilder.DropColumn( + name: "TwitchClientSecret", + table: "SiteSettings"); + + migrationBuilder.DropColumn( + name: "TwitchRedirectUri", + table: "SiteSettings"); + + migrationBuilder.DropColumn( + name: "TwitchScope", + table: "SiteSettings"); + + migrationBuilder.CreateIndex( + name: "IX_VoteBallots_SeasonId", + table: "VoteBallots", + column: "SeasonId"); + } + } +} diff --git a/Backend/Migrations/20260627114301_AddCandidatePreparationFields.Designer.cs b/Backend/Migrations/20260627114301_AddCandidatePreparationFields.Designer.cs new file mode 100644 index 0000000..1a2531f --- /dev/null +++ b/Backend/Migrations/20260627114301_AddCandidatePreparationFields.Designer.cs @@ -0,0 +1,1527 @@ +// +using System; +using Backend.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backend.Migrations +{ + [DbContext(typeof(AwardsDbContext))] + [Migration("20260627114301_AddCandidatePreparationFields")] + partial class AddCandidatePreparationFields + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backend.Domain.AdminAuditEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("AdminTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.ToTable("AdminAuditEntries"); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CategoryName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "CategoryId") + .IsUnique(); + + b.ToTable("Results"); + + b.HasData( + new + { + Id = 1, + CandidateId = 8, + CategoryId = 5, + CategoryName = "VTuber des Jahres", + SeasonId = 2 + }, + new + { + Id = 2, + CandidateId = 9, + CategoryId = 6, + CategoryName = "Bestes Live Event", + SeasonId = 2 + }, + new + { + Id = 3, + CandidateId = 10, + CategoryId = 7, + CategoryName = "Clip des Jahres", + SeasonId = 2 + }, + new + { + Id = 4, + CandidateId = 11, + CategoryId = 8, + CategoryName = "VTuber des Jahres", + SeasonId = 3 + }, + new + { + Id = 5, + CandidateId = 12, + CategoryId = 9, + CategoryName = "Clip des Jahres", + SeasonId = 3 + }, + new + { + Id = 6, + CandidateId = 13, + CategoryId = 10, + CategoryName = "VTuber des Jahres", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AcceptanceNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AcceptanceStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasDefaultValue("open"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ChannelSlug") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ClipCompilationPlatform") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ClipCompilationTitle") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ClipCompilationUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ClipEmbedStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasDefaultValue("unchecked"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId"); + + b.ToTable("Candidates"); + + b.HasData( + new + { + Id = 1, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@hoshimimiyu", + ClipEmbedStatus = "unchecked", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 2, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 3, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@shiroch", + ClipEmbedStatus = "unchecked", + DisplayName = "Shiro Ch.", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 4, + AcceptanceStatus = "open", + CategoryId = 2, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 5, + AcceptanceStatus = "open", + CategoryId = 2, + ChannelSlug = "@aoisakura", + ClipEmbedStatus = "unchecked", + DisplayName = "Aoi Sakura Showcase", + Platform = "YouTube", + SeasonId = 1 + }, + new + { + Id = 6, + AcceptanceStatus = "open", + CategoryId = 3, + ChannelSlug = "@pyonkichikingdom", + ClipEmbedStatus = "unchecked", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 7, + AcceptanceStatus = "open", + CategoryId = 4, + ChannelSlug = "@moonrelay", + ClipEmbedStatus = "unchecked", + DisplayName = "Moonrelay", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 8, + AcceptanceStatus = "open", + CategoryId = 5, + ChannelSlug = "@hoshimimiyu", + ClipEmbedStatus = "unchecked", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 9, + AcceptanceStatus = "open", + CategoryId = 6, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 10, + AcceptanceStatus = "open", + CategoryId = 7, + ChannelSlug = "@pyonkichikingdom", + ClipEmbedStatus = "unchecked", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 11, + AcceptanceStatus = "open", + CategoryId = 8, + ChannelSlug = "@aoisakura", + ClipEmbedStatus = "unchecked", + DisplayName = "Aoi Sakura", + Platform = "YouTube", + SeasonId = 3 + }, + new + { + Id = 12, + AcceptanceStatus = "open", + CategoryId = 9, + ChannelSlug = "@starbyte", + ClipEmbedStatus = "unchecked", + DisplayName = "Starbyte", + Platform = "Twitch", + SeasonId = 3 + }, + new + { + Id = 13, + AcceptanceStatus = "open", + CategoryId = 10, + ChannelSlug = "@tenshivox", + ClipEmbedStatus = "unchecked", + DisplayName = "Tenshi Vox", + Platform = "Twitch", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MaxNomineesPerUser") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Slug") + .IsUnique(); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = 1, + Description = "Die groesste Auszeichnung des Jahres.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 1, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 2, + Description = "Events, Konzerte und 3D-Shows.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 1, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 3, + Description = "Der lustigste oder emotionalste Clip des Jahres.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 1, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 4, + Description = "Die aktivste und freundlichste Community.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "Beste Community", + SeasonId = 1, + Slug = "beste-community", + SortOrder = 4 + }, + new + { + Id = 5, + Description = "Archivkategorie 2025.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 2, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 6, + Description = "Archivkategorie 2025.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 2, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 7, + Description = "Archivkategorie 2025.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 2, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 8, + Description = "Archivkategorie 2024.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 3, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 9, + Description = "Archivkategorie 2024.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 3, + Slug = "clip-des-jahres", + SortOrder = 2 + }, + new + { + Id = 10, + Description = "Archivkategorie 2023.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 4, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ClipUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Creator") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("ClipSubmissions"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CandidateText") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("StreamUrl") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("Nominations"); + + b.HasData( + new + { + Id = 1, + CandidateText = "Hoshimi Miyu", + CategoryId = 1, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 13, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_hoshi" + }, + new + { + Id = 2, + CandidateText = "Kurainu 3D Live", + CategoryId = 2, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 14, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_kurainu" + }); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("TwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("RiskFlags"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrentPhase") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("IsCommunityOnly") + .HasColumnType("boolean"); + + b.Property("IsCurrent") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("NominationEndsAt") + .HasColumnType("date"); + + b.Property("NominationStartsAt") + .HasColumnType("date"); + + b.Property("ReviewEndsAt") + .HasColumnType("date"); + + b.Property("ReviewStartsAt") + .HasColumnType("date"); + + b.Property("ShowDate") + .HasColumnType("date"); + + b.Property("ShowStartsAt") + .HasColumnType("time without time zone"); + + b.Property("ShowStreamUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("VotingEndsAt") + .HasColumnType("date"); + + b.Property("VotingStartsAt") + .HasColumnType("date"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("Seasons"); + + b.HasData( + new + { + Id = 1, + CurrentPhase = "Community Voting", + IsCommunityOnly = true, + IsCurrent = true, + Name = "VTuber Star Awards 2026", + NominationEndsAt = new DateOnly(2026, 5, 31), + NominationStartsAt = new DateOnly(2026, 5, 1), + ReviewEndsAt = new DateOnly(2026, 7, 10), + ReviewStartsAt = new DateOnly(2026, 7, 1), + ShowDate = new DateOnly(2026, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2026, 6, 30), + VotingStartsAt = new DateOnly(2026, 6, 1), + Year = 2026 + }, + new + { + Id = 2, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2025", + NominationEndsAt = new DateOnly(2025, 5, 31), + NominationStartsAt = new DateOnly(2025, 5, 1), + ReviewEndsAt = new DateOnly(2025, 7, 10), + ReviewStartsAt = new DateOnly(2025, 7, 1), + ShowDate = new DateOnly(2025, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2025, 6, 30), + VotingStartsAt = new DateOnly(2025, 6, 1), + Year = 2025 + }, + new + { + Id = 3, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2024", + NominationEndsAt = new DateOnly(2024, 5, 31), + NominationStartsAt = new DateOnly(2024, 5, 1), + ReviewEndsAt = new DateOnly(2024, 7, 10), + ReviewStartsAt = new DateOnly(2024, 7, 1), + ShowDate = new DateOnly(2024, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://youtube.com/c/Jayuhime", + VotingEndsAt = new DateOnly(2024, 6, 30), + VotingStartsAt = new DateOnly(2024, 6, 1), + Year = 2024 + }, + new + { + Id = 4, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2023", + NominationEndsAt = new DateOnly(2023, 5, 31), + NominationStartsAt = new DateOnly(2023, 5, 1), + ReviewEndsAt = new DateOnly(2023, 7, 10), + ReviewStartsAt = new DateOnly(2023, 7, 1), + ShowDate = new DateOnly(2023, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2023, 6, 30), + VotingStartsAt = new DateOnly(2023, 6, 1), + Year = 2023 + }); + }); + + modelBuilder.Entity("Backend.Domain.SiteSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContactContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("DemoLoginDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginEmail") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("DemoLoginEnabled") + .HasColumnType("boolean"); + + b.Property("DemoLoginManagedByDatabase") + .HasColumnType("boolean"); + + b.Property("DemoLoginPasswordHash") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginPasswordSalt") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DemoLoginTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("FaqJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("HostDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("HostTagline") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("ImprintContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("ImprintUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("MaintenanceMessage") + .IsRequired() + .HasMaxLength(600) + .HasColumnType("character varying(600)"); + + b.Property("MaintenanceModeEnabled") + .HasColumnType("boolean"); + + b.Property("MaintenanceTitle") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("NewsletterUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("PrivacyEmail") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("PrivacyPolicyContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("PrivacyPolicyUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivacyPolicyUpdatedBy") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("RiskRulesJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SocialLinksJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("TwitchAuthManagedByDatabase") + .HasColumnType("boolean"); + + b.Property("TwitchClientId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchClientSecret") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("TwitchRedirectUri") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("TwitchScope") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.HasKey("Id"); + + b.ToTable("SiteSettings"); + + b.HasData( + new + { + Id = 1, + ContactContent = "Kontakt zum Award-Team\nDu hast Fragen zur Nominierung, zum Voting, zu Clips oder zur Show? Schreib dem Team ueber die hinterlegte Kontaktseite.\n\nDatenschutzfragen\nFuer Datenschutzanfragen nutze bitte die Datenschutz-E-Mail aus diesem Footer.\n\nCommunity & Kooperationen\nSocial Links und Partneranfragen werden vom Admin-Team gepflegt und koordiniert.", + ContactUrl = "https://vtuber-star-awards.de/kontakt", + DemoLoginDisplayName = "Jayuhime Admin", + DemoLoginEmail = "", + DemoLoginEnabled = false, + DemoLoginManagedByDatabase = false, + DemoLoginPasswordHash = "", + DemoLoginPasswordSalt = "", + DemoLoginTwitchUserId = "jayuhime_admin", + FaqJson = "[{\"question\":\"Wer darf nominiert werden?\",\"answer\":\"Jede:r aktive deutschsprachige VTuber kann nominiert werden \\u2014 unabh\\u00E4ngig von Follower-Zahl oder Plattform. Die Community schl\\u00E4gt in der Nominierungsphase ihre Favorit:innen vor.\"},{\"question\":\"Wie funktioniert das Voting?\",\"answer\":\"Du meldest dich ausschlie\\u00DFlich mit deinem Twitch-Account an \\u2014 nur so kannst du teilnehmen. Das h\\u00E4lt das Voting fair und bot-frei. Danach vergibst du pro Kategorie eine Stimme, \\u00E4nderbar bis zum Ende der Phase.\"},{\"question\":\"Was kostet die Teilnahme?\",\"answer\":\"Gar nichts. Nominieren, voten und die Show schauen ist komplett kostenlos \\u2014 der VTuber Star Award ist ein Community-Event von Fans f\\u00FCr Fans.\"},{\"question\":\"Wann und wo findet die Award-Show statt?\",\"answer\":\"Die gro\\u00DFe Live-Show wird von Jayuhime gehostet und auf Twitch \\u0026 YouTube gestreamt. Den genauen Termin findest du im Countdown oben \\u2014 sei live dabei, wenn die Stars gek\\u00FCrt werden!\"},{\"question\":\"Ich wurde nominiert \\u2014 was nun?\",\"answer\":\"Gl\\u00FCckwunsch! Du erh\\u00E4ltst eine Benachrichtigung mit allen Infos. Teile deine Nominierung gern mit deiner Community und ruf zum Voten auf \\u2014 jede Stimme z\\u00E4hlt.\"}]", + HostDisplayName = "Jayuhime", + HostTagline = "VTuber & Award Host", + ImprintContent = "Anbieter\nVTuber Star Awards, vertreten durch Jayuhime.\n\nKontakt\nNutze fuer organisatorische Fragen bitte die Kontaktseite oder die hinterlegte Kontaktadresse.\n\nHinweis\nDieses Impressum ist ein redaktioneller Platzhalter und sollte vor dem Livegang mit den tatsaechlichen Anbieterangaben ersetzt werden.", + ImprintUrl = "https://vtuber-star-awards.de/impressum", + MaintenanceMessage = "Die Award-Galaxie wird gerade liebevoll poliert. Schau gleich wieder vorbei.", + MaintenanceModeEnabled = false, + MaintenanceTitle = "Sternenpause", + NewsletterUrl = "https://vtuber-star-awards.de/newsletter", + PrivacyEmail = "datenschutz@vtuber-star-awards.de", + PrivacyPolicyContent = "Verantwortliche:r\nVTuber Star Awards, vertreten durch Jayuhime. Kontakt: datenschutz@vtuber-star-awards.de\n\nWelche Daten wir verarbeiten\nBei Teilnahme (Voting, Nominierung, Clip-Einreichung) verarbeiten wir ausschließlich deine Twitch-User-ID sowie den Zeitstempel deiner Aktion.\nFür Show-Erinnerungen speichern wir optional deine E-Mail-Adresse.\n\nRechtsgrundlage\nVerarbeitung auf Basis von Art. 6 Abs. 1 lit. b DSGVO. Für optionale Erinnerungen gilt Art. 6 Abs. 1 lit. a DSGVO.\n\nZweck der Verarbeitung\nDurchführung des VTuber Star Awards, Sicherstellung fairer Abstimmung, Spam-Prävention sowie Admin-Review von Einreichungen.\n\nLöschfristen\nAlle Teilnahmedaten werden spätestens 6 Monate nach der Award-Show automatisch gelöscht. E-Mail-Adressen werden nach der Show entfernt.\n\nDeine Rechte\nDu hast das Recht auf Auskunft, Löschung, Berichtigung, Widerspruch und Beschwerde bei der zuständigen Aufsichtsbehörde.\n\nWeitergabe an Dritte\nKeine Weitergabe zu Werbezwecken. Technische Dienstleister verarbeiten Daten ausschließlich im Rahmen der Auftragsverarbeitung.", + PrivacyPolicyUpdatedAt = new DateTimeOffset(new DateTime(2026, 6, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + PrivacyPolicyUpdatedBy = "seed", + RiskRulesJson = "[{\"key\":\"resubmitted_ballot\",\"label\":\"Ballot erneut gespeichert\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"low\",\"description\":\"Erstellt einen Hinweis, wenn ein User sein Voting erneut speichert.\"},{\"key\":\"rapid_vote_updates\",\"label\":\"Voting-Burst\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr schnell mehrere Voting-Aenderungen ausloest.\"},{\"key\":\"resubmitted_nomination\",\"label\":\"Nominierung erneut eingereicht\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"low\",\"description\":\"Erstellt einen Hinweis, wenn ein User in derselben Kategorie erneut nominiert.\"},{\"key\":\"rapid_nomination_burst\",\"label\":\"Nominierungs-Burst\",\"enabled\":true,\"threshold\":10,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr viele Nominierungen in kurzer Zeit sendet.\"},{\"key\":\"duplicate_clip_submission\",\"label\":\"Doppelter Clip\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn ein User denselben Clip erneut einreicht.\"},{\"key\":\"rapid_clip_burst\",\"label\":\"Clip-Burst\",\"enabled\":true,\"threshold\":5,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr viele Clips in kurzer Zeit sendet.\"},{\"key\":\"rapid_login_ip\",\"label\":\"Login-Burst pro IP\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":15,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn mehrere neue Sessions von derselben IP entstehen.\"},{\"key\":\"rapid_demo_login_ip\",\"label\":\"Demo-Login-Burst pro IP\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":15,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn mehrere Demo-Admin-Sessions von derselben IP entstehen.\"}]", + SocialLinksJson = "[{\"label\":\"Twitch\",\"platform\":\"twitch\",\"url\":\"https://twitch.tv/jayuhime\",\"icon\":\"twitch\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"YouTube\",\"platform\":\"youtube\",\"url\":\"https://youtube.com/c/Jayuhime\",\"icon\":\"youtube\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"X\",\"platform\":\"x\",\"url\":\"https://x.com/jayuhime\",\"icon\":\"x\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Instagram\",\"platform\":\"instagram\",\"url\":\"https://instagram.com/jayuhime\",\"icon\":\"instagram\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Discord\",\"platform\":\"discord\",\"url\":\"https://discord.gg/jayuhime\",\"icon\":\"discord\",\"showOnHost\":true,\"showOnCommunity\":true}]", + SponsorsContent = "Sponsoren & Partner\nHier koennen Sponsor:innen, Medienpartner, Community-Partner und Supporter des VTuber Star Awards vorgestellt werden.\n\nPartner werden im Rahmen der Show und auf den oeffentlichen Kontaktflaechen genannt, sobald sie final bestaetigt sind.", + SponsorsUrl = "https://vtuber-star-awards.de/partner", + TwitchAuthManagedByDatabase = false, + TwitchClientId = "", + TwitchClientSecret = "", + TwitchRedirectUri = "", + TwitchScope = "" + }); + }); + + modelBuilder.Entity("Backend.Domain.TeamMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoundTwitchDisplayName") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("BoundTwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Login") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MustChangePassword") + .HasColumnType("boolean"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("PasswordResetAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("TwitchBoundAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("BoundTwitchUserId") + .IsUnique(); + + b.HasIndex("Login") + .IsUnique(); + + b.ToTable("TeamMembers"); + }); + + modelBuilder.Entity("Backend.Domain.TeamRolePermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PermissionsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("Role") + .IsUnique(); + + b.ToTable("TeamRolePermissions"); + }); + + modelBuilder.Entity("Backend.Domain.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SessionToken") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SessionToken") + .IsUnique(); + + b.ToTable("UserSessions"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "SubmittedByTwitchId") + .IsUnique(); + + b.ToTable("VoteBallots"); + + b.HasData( + new + { + Id = 1, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_1" + }, + new + { + Id = 2, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 5, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_2" + }); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BallotId") + .HasColumnType("integer"); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BallotId"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.ToTable("VoteEntries"); + + b.HasData( + new + { + Id = 1, + BallotId = 1, + CandidateId = 1, + CategoryId = 1 + }, + new + { + Id = 2, + BallotId = 1, + CandidateId = 4, + CategoryId = 2 + }, + new + { + Id = 3, + BallotId = 2, + CandidateId = 2, + CategoryId = 1 + }, + new + { + Id = 4, + BallotId = 2, + CandidateId = 6, + CategoryId = 3 + }); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Results") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.HasOne("Backend.Domain.Category", "Category") + .WithMany("Candidates") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Categories") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Candidate"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId"); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.HasOne("Backend.Domain.VoteBallot", "Ballot") + .WithMany("Entries") + .HasForeignKey("BallotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ballot"); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Navigation("Candidates"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Navigation("Categories"); + + b.Navigation("Results"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Navigation("Entries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/Migrations/20260627114301_AddCandidatePreparationFields.cs b/Backend/Migrations/20260627114301_AddCandidatePreparationFields.cs new file mode 100644 index 0000000..178b2b4 --- /dev/null +++ b/Backend/Migrations/20260627114301_AddCandidatePreparationFields.cs @@ -0,0 +1,177 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backend.Migrations +{ + /// + public partial class AddCandidatePreparationFields : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AcceptanceNote", + table: "Candidates", + type: "character varying(500)", + maxLength: 500, + nullable: true); + + migrationBuilder.AddColumn( + name: "AcceptanceStatus", + table: "Candidates", + type: "character varying(30)", + maxLength: 30, + nullable: false, + defaultValue: "open"); + + migrationBuilder.AddColumn( + name: "ClipCompilationPlatform", + table: "Candidates", + type: "character varying(40)", + maxLength: 40, + nullable: true); + + migrationBuilder.AddColumn( + name: "ClipCompilationTitle", + table: "Candidates", + type: "character varying(200)", + maxLength: 200, + nullable: true); + + migrationBuilder.AddColumn( + name: "ClipCompilationUrl", + table: "Candidates", + type: "character varying(500)", + maxLength: 500, + nullable: true); + + migrationBuilder.AddColumn( + name: "ClipEmbedStatus", + table: "Candidates", + type: "character varying(30)", + maxLength: 30, + nullable: false, + defaultValue: "unchecked"); + + migrationBuilder.UpdateData( + table: "Candidates", + keyColumn: "Id", + keyValue: 1, + columns: new[] { "AcceptanceNote", "AcceptanceStatus", "ClipCompilationPlatform", "ClipCompilationTitle", "ClipCompilationUrl", "ClipEmbedStatus" }, + values: new object[] { null, "open", null, null, null, "unchecked" }); + + migrationBuilder.UpdateData( + table: "Candidates", + keyColumn: "Id", + keyValue: 2, + columns: new[] { "AcceptanceNote", "AcceptanceStatus", "ClipCompilationPlatform", "ClipCompilationTitle", "ClipCompilationUrl", "ClipEmbedStatus" }, + values: new object[] { null, "open", null, null, null, "unchecked" }); + + migrationBuilder.UpdateData( + table: "Candidates", + keyColumn: "Id", + keyValue: 3, + columns: new[] { "AcceptanceNote", "AcceptanceStatus", "ClipCompilationPlatform", "ClipCompilationTitle", "ClipCompilationUrl", "ClipEmbedStatus" }, + values: new object[] { null, "open", null, null, null, "unchecked" }); + + migrationBuilder.UpdateData( + table: "Candidates", + keyColumn: "Id", + keyValue: 4, + columns: new[] { "AcceptanceNote", "AcceptanceStatus", "ClipCompilationPlatform", "ClipCompilationTitle", "ClipCompilationUrl", "ClipEmbedStatus" }, + values: new object[] { null, "open", null, null, null, "unchecked" }); + + migrationBuilder.UpdateData( + table: "Candidates", + keyColumn: "Id", + keyValue: 5, + columns: new[] { "AcceptanceNote", "AcceptanceStatus", "ClipCompilationPlatform", "ClipCompilationTitle", "ClipCompilationUrl", "ClipEmbedStatus" }, + values: new object[] { null, "open", null, null, null, "unchecked" }); + + migrationBuilder.UpdateData( + table: "Candidates", + keyColumn: "Id", + keyValue: 6, + columns: new[] { "AcceptanceNote", "AcceptanceStatus", "ClipCompilationPlatform", "ClipCompilationTitle", "ClipCompilationUrl", "ClipEmbedStatus" }, + values: new object[] { null, "open", null, null, null, "unchecked" }); + + migrationBuilder.UpdateData( + table: "Candidates", + keyColumn: "Id", + keyValue: 7, + columns: new[] { "AcceptanceNote", "AcceptanceStatus", "ClipCompilationPlatform", "ClipCompilationTitle", "ClipCompilationUrl", "ClipEmbedStatus" }, + values: new object[] { null, "open", null, null, null, "unchecked" }); + + migrationBuilder.UpdateData( + table: "Candidates", + keyColumn: "Id", + keyValue: 8, + columns: new[] { "AcceptanceNote", "AcceptanceStatus", "ClipCompilationPlatform", "ClipCompilationTitle", "ClipCompilationUrl", "ClipEmbedStatus" }, + values: new object[] { null, "open", null, null, null, "unchecked" }); + + migrationBuilder.UpdateData( + table: "Candidates", + keyColumn: "Id", + keyValue: 9, + columns: new[] { "AcceptanceNote", "AcceptanceStatus", "ClipCompilationPlatform", "ClipCompilationTitle", "ClipCompilationUrl", "ClipEmbedStatus" }, + values: new object[] { null, "open", null, null, null, "unchecked" }); + + migrationBuilder.UpdateData( + table: "Candidates", + keyColumn: "Id", + keyValue: 10, + columns: new[] { "AcceptanceNote", "AcceptanceStatus", "ClipCompilationPlatform", "ClipCompilationTitle", "ClipCompilationUrl", "ClipEmbedStatus" }, + values: new object[] { null, "open", null, null, null, "unchecked" }); + + migrationBuilder.UpdateData( + table: "Candidates", + keyColumn: "Id", + keyValue: 11, + columns: new[] { "AcceptanceNote", "AcceptanceStatus", "ClipCompilationPlatform", "ClipCompilationTitle", "ClipCompilationUrl", "ClipEmbedStatus" }, + values: new object[] { null, "open", null, null, null, "unchecked" }); + + migrationBuilder.UpdateData( + table: "Candidates", + keyColumn: "Id", + keyValue: 12, + columns: new[] { "AcceptanceNote", "AcceptanceStatus", "ClipCompilationPlatform", "ClipCompilationTitle", "ClipCompilationUrl", "ClipEmbedStatus" }, + values: new object[] { null, "open", null, null, null, "unchecked" }); + + migrationBuilder.UpdateData( + table: "Candidates", + keyColumn: "Id", + keyValue: 13, + columns: new[] { "AcceptanceNote", "AcceptanceStatus", "ClipCompilationPlatform", "ClipCompilationTitle", "ClipCompilationUrl", "ClipEmbedStatus" }, + values: new object[] { null, "open", null, null, null, "unchecked" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "AcceptanceNote", + table: "Candidates"); + + migrationBuilder.DropColumn( + name: "AcceptanceStatus", + table: "Candidates"); + + migrationBuilder.DropColumn( + name: "ClipCompilationPlatform", + table: "Candidates"); + + migrationBuilder.DropColumn( + name: "ClipCompilationTitle", + table: "Candidates"); + + migrationBuilder.DropColumn( + name: "ClipCompilationUrl", + table: "Candidates"); + + migrationBuilder.DropColumn( + name: "ClipEmbedStatus", + table: "Candidates"); + } + } +} diff --git a/Backend/Migrations/20260627125128_AddWorkflowRulesJson.Designer.cs b/Backend/Migrations/20260627125128_AddWorkflowRulesJson.Designer.cs new file mode 100644 index 0000000..f537bb7 --- /dev/null +++ b/Backend/Migrations/20260627125128_AddWorkflowRulesJson.Designer.cs @@ -0,0 +1,1534 @@ +// +using System; +using Backend.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backend.Migrations +{ + [DbContext(typeof(AwardsDbContext))] + [Migration("20260627125128_AddWorkflowRulesJson")] + partial class AddWorkflowRulesJson + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backend.Domain.AdminAuditEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("AdminTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.ToTable("AdminAuditEntries"); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CategoryName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "CategoryId") + .IsUnique(); + + b.ToTable("Results"); + + b.HasData( + new + { + Id = 1, + CandidateId = 8, + CategoryId = 5, + CategoryName = "VTuber des Jahres", + SeasonId = 2 + }, + new + { + Id = 2, + CandidateId = 9, + CategoryId = 6, + CategoryName = "Bestes Live Event", + SeasonId = 2 + }, + new + { + Id = 3, + CandidateId = 10, + CategoryId = 7, + CategoryName = "Clip des Jahres", + SeasonId = 2 + }, + new + { + Id = 4, + CandidateId = 11, + CategoryId = 8, + CategoryName = "VTuber des Jahres", + SeasonId = 3 + }, + new + { + Id = 5, + CandidateId = 12, + CategoryId = 9, + CategoryName = "Clip des Jahres", + SeasonId = 3 + }, + new + { + Id = 6, + CandidateId = 13, + CategoryId = 10, + CategoryName = "VTuber des Jahres", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AcceptanceNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AcceptanceStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasDefaultValue("open"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ChannelSlug") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ClipCompilationPlatform") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ClipCompilationTitle") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ClipCompilationUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ClipEmbedStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasDefaultValue("unchecked"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId"); + + b.ToTable("Candidates"); + + b.HasData( + new + { + Id = 1, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@hoshimimiyu", + ClipEmbedStatus = "unchecked", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 2, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 3, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@shiroch", + ClipEmbedStatus = "unchecked", + DisplayName = "Shiro Ch.", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 4, + AcceptanceStatus = "open", + CategoryId = 2, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 5, + AcceptanceStatus = "open", + CategoryId = 2, + ChannelSlug = "@aoisakura", + ClipEmbedStatus = "unchecked", + DisplayName = "Aoi Sakura Showcase", + Platform = "YouTube", + SeasonId = 1 + }, + new + { + Id = 6, + AcceptanceStatus = "open", + CategoryId = 3, + ChannelSlug = "@pyonkichikingdom", + ClipEmbedStatus = "unchecked", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 7, + AcceptanceStatus = "open", + CategoryId = 4, + ChannelSlug = "@moonrelay", + ClipEmbedStatus = "unchecked", + DisplayName = "Moonrelay", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 8, + AcceptanceStatus = "open", + CategoryId = 5, + ChannelSlug = "@hoshimimiyu", + ClipEmbedStatus = "unchecked", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 9, + AcceptanceStatus = "open", + CategoryId = 6, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 10, + AcceptanceStatus = "open", + CategoryId = 7, + ChannelSlug = "@pyonkichikingdom", + ClipEmbedStatus = "unchecked", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 11, + AcceptanceStatus = "open", + CategoryId = 8, + ChannelSlug = "@aoisakura", + ClipEmbedStatus = "unchecked", + DisplayName = "Aoi Sakura", + Platform = "YouTube", + SeasonId = 3 + }, + new + { + Id = 12, + AcceptanceStatus = "open", + CategoryId = 9, + ChannelSlug = "@starbyte", + ClipEmbedStatus = "unchecked", + DisplayName = "Starbyte", + Platform = "Twitch", + SeasonId = 3 + }, + new + { + Id = 13, + AcceptanceStatus = "open", + CategoryId = 10, + ChannelSlug = "@tenshivox", + ClipEmbedStatus = "unchecked", + DisplayName = "Tenshi Vox", + Platform = "Twitch", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MaxNomineesPerUser") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Slug") + .IsUnique(); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = 1, + Description = "Die groesste Auszeichnung des Jahres.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 1, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 2, + Description = "Events, Konzerte und 3D-Shows.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 1, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 3, + Description = "Der lustigste oder emotionalste Clip des Jahres.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 1, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 4, + Description = "Die aktivste und freundlichste Community.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "Beste Community", + SeasonId = 1, + Slug = "beste-community", + SortOrder = 4 + }, + new + { + Id = 5, + Description = "Archivkategorie 2025.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 2, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 6, + Description = "Archivkategorie 2025.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 2, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 7, + Description = "Archivkategorie 2025.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 2, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 8, + Description = "Archivkategorie 2024.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 3, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 9, + Description = "Archivkategorie 2024.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 3, + Slug = "clip-des-jahres", + SortOrder = 2 + }, + new + { + Id = 10, + Description = "Archivkategorie 2023.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 4, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ClipUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Creator") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("ClipSubmissions"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CandidateText") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("StreamUrl") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("Nominations"); + + b.HasData( + new + { + Id = 1, + CandidateText = "Hoshimi Miyu", + CategoryId = 1, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 13, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_hoshi" + }, + new + { + Id = 2, + CandidateText = "Kurainu 3D Live", + CategoryId = 2, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 14, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_kurainu" + }); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("TwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("RiskFlags"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrentPhase") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("IsCommunityOnly") + .HasColumnType("boolean"); + + b.Property("IsCurrent") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("NominationEndsAt") + .HasColumnType("date"); + + b.Property("NominationStartsAt") + .HasColumnType("date"); + + b.Property("ReviewEndsAt") + .HasColumnType("date"); + + b.Property("ReviewStartsAt") + .HasColumnType("date"); + + b.Property("ShowDate") + .HasColumnType("date"); + + b.Property("ShowStartsAt") + .HasColumnType("time without time zone"); + + b.Property("ShowStreamUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("VotingEndsAt") + .HasColumnType("date"); + + b.Property("VotingStartsAt") + .HasColumnType("date"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("Seasons"); + + b.HasData( + new + { + Id = 1, + CurrentPhase = "Community Voting", + IsCommunityOnly = true, + IsCurrent = true, + Name = "VTuber Star Awards 2026", + NominationEndsAt = new DateOnly(2026, 5, 31), + NominationStartsAt = new DateOnly(2026, 5, 1), + ReviewEndsAt = new DateOnly(2026, 7, 10), + ReviewStartsAt = new DateOnly(2026, 7, 1), + ShowDate = new DateOnly(2026, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2026, 6, 30), + VotingStartsAt = new DateOnly(2026, 6, 1), + Year = 2026 + }, + new + { + Id = 2, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2025", + NominationEndsAt = new DateOnly(2025, 5, 31), + NominationStartsAt = new DateOnly(2025, 5, 1), + ReviewEndsAt = new DateOnly(2025, 7, 10), + ReviewStartsAt = new DateOnly(2025, 7, 1), + ShowDate = new DateOnly(2025, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2025, 6, 30), + VotingStartsAt = new DateOnly(2025, 6, 1), + Year = 2025 + }, + new + { + Id = 3, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2024", + NominationEndsAt = new DateOnly(2024, 5, 31), + NominationStartsAt = new DateOnly(2024, 5, 1), + ReviewEndsAt = new DateOnly(2024, 7, 10), + ReviewStartsAt = new DateOnly(2024, 7, 1), + ShowDate = new DateOnly(2024, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://youtube.com/c/Jayuhime", + VotingEndsAt = new DateOnly(2024, 6, 30), + VotingStartsAt = new DateOnly(2024, 6, 1), + Year = 2024 + }, + new + { + Id = 4, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2023", + NominationEndsAt = new DateOnly(2023, 5, 31), + NominationStartsAt = new DateOnly(2023, 5, 1), + ReviewEndsAt = new DateOnly(2023, 7, 10), + ReviewStartsAt = new DateOnly(2023, 7, 1), + ShowDate = new DateOnly(2023, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2023, 6, 30), + VotingStartsAt = new DateOnly(2023, 6, 1), + Year = 2023 + }); + }); + + modelBuilder.Entity("Backend.Domain.SiteSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContactContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("DemoLoginDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginEmail") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("DemoLoginEnabled") + .HasColumnType("boolean"); + + b.Property("DemoLoginManagedByDatabase") + .HasColumnType("boolean"); + + b.Property("DemoLoginPasswordHash") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginPasswordSalt") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DemoLoginTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("FaqJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("HostDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("HostTagline") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("ImprintContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("ImprintUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("MaintenanceMessage") + .IsRequired() + .HasMaxLength(600) + .HasColumnType("character varying(600)"); + + b.Property("MaintenanceModeEnabled") + .HasColumnType("boolean"); + + b.Property("MaintenanceTitle") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("NewsletterUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("PrivacyEmail") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("PrivacyPolicyContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("PrivacyPolicyUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivacyPolicyUpdatedBy") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("RiskRulesJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SocialLinksJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("TwitchAuthManagedByDatabase") + .HasColumnType("boolean"); + + b.Property("TwitchClientId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchClientSecret") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("TwitchRedirectUri") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("TwitchScope") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("WorkflowRulesJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("[]"); + + b.HasKey("Id"); + + b.ToTable("SiteSettings"); + + b.HasData( + new + { + Id = 1, + ContactContent = "Kontakt zum Award-Team\nDu hast Fragen zur Nominierung, zum Voting, zu Clips oder zur Show? Schreib dem Team ueber die hinterlegte Kontaktseite.\n\nDatenschutzfragen\nFuer Datenschutzanfragen nutze bitte die Datenschutz-E-Mail aus diesem Footer.\n\nCommunity & Kooperationen\nSocial Links und Partneranfragen werden vom Admin-Team gepflegt und koordiniert.", + ContactUrl = "https://vtuber-star-awards.de/kontakt", + DemoLoginDisplayName = "Jayuhime Admin", + DemoLoginEmail = "", + DemoLoginEnabled = false, + DemoLoginManagedByDatabase = false, + DemoLoginPasswordHash = "", + DemoLoginPasswordSalt = "", + DemoLoginTwitchUserId = "jayuhime_admin", + FaqJson = "[{\"question\":\"Wer darf nominiert werden?\",\"answer\":\"Jede:r aktive deutschsprachige VTuber kann nominiert werden \\u2014 unabh\\u00E4ngig von Follower-Zahl oder Plattform. Die Community schl\\u00E4gt in der Nominierungsphase ihre Favorit:innen vor.\"},{\"question\":\"Wie funktioniert das Voting?\",\"answer\":\"Du meldest dich ausschlie\\u00DFlich mit deinem Twitch-Account an \\u2014 nur so kannst du teilnehmen. Das h\\u00E4lt das Voting fair und bot-frei. Danach vergibst du pro Kategorie eine Stimme, \\u00E4nderbar bis zum Ende der Phase.\"},{\"question\":\"Was kostet die Teilnahme?\",\"answer\":\"Gar nichts. Nominieren, voten und die Show schauen ist komplett kostenlos \\u2014 der VTuber Star Award ist ein Community-Event von Fans f\\u00FCr Fans.\"},{\"question\":\"Wann und wo findet die Award-Show statt?\",\"answer\":\"Die gro\\u00DFe Live-Show wird von Jayuhime gehostet und auf Twitch \\u0026 YouTube gestreamt. Den genauen Termin findest du im Countdown oben \\u2014 sei live dabei, wenn die Stars gek\\u00FCrt werden!\"},{\"question\":\"Ich wurde nominiert \\u2014 was nun?\",\"answer\":\"Gl\\u00FCckwunsch! Du erh\\u00E4ltst eine Benachrichtigung mit allen Infos. Teile deine Nominierung gern mit deiner Community und ruf zum Voten auf \\u2014 jede Stimme z\\u00E4hlt.\"}]", + HostDisplayName = "Jayuhime", + HostTagline = "VTuber & Award Host", + ImprintContent = "Anbieter\nVTuber Star Awards, vertreten durch Jayuhime.\n\nKontakt\nNutze fuer organisatorische Fragen bitte die Kontaktseite oder die hinterlegte Kontaktadresse.\n\nHinweis\nDieses Impressum ist ein redaktioneller Platzhalter und sollte vor dem Livegang mit den tatsaechlichen Anbieterangaben ersetzt werden.", + ImprintUrl = "https://vtuber-star-awards.de/impressum", + MaintenanceMessage = "Die Award-Galaxie wird gerade liebevoll poliert. Schau gleich wieder vorbei.", + MaintenanceModeEnabled = false, + MaintenanceTitle = "Sternenpause", + NewsletterUrl = "https://vtuber-star-awards.de/newsletter", + PrivacyEmail = "datenschutz@vtuber-star-awards.de", + PrivacyPolicyContent = "Verantwortliche:r\nVTuber Star Awards, vertreten durch Jayuhime. Kontakt: datenschutz@vtuber-star-awards.de\n\nWelche Daten wir verarbeiten\nBei Teilnahme (Voting, Nominierung, Clip-Einreichung) verarbeiten wir ausschließlich deine Twitch-User-ID sowie den Zeitstempel deiner Aktion.\nFür Show-Erinnerungen speichern wir optional deine E-Mail-Adresse.\n\nRechtsgrundlage\nVerarbeitung auf Basis von Art. 6 Abs. 1 lit. b DSGVO. Für optionale Erinnerungen gilt Art. 6 Abs. 1 lit. a DSGVO.\n\nZweck der Verarbeitung\nDurchführung des VTuber Star Awards, Sicherstellung fairer Abstimmung, Spam-Prävention sowie Admin-Review von Einreichungen.\n\nLöschfristen\nAlle Teilnahmedaten werden spätestens 6 Monate nach der Award-Show automatisch gelöscht. E-Mail-Adressen werden nach der Show entfernt.\n\nDeine Rechte\nDu hast das Recht auf Auskunft, Löschung, Berichtigung, Widerspruch und Beschwerde bei der zuständigen Aufsichtsbehörde.\n\nWeitergabe an Dritte\nKeine Weitergabe zu Werbezwecken. Technische Dienstleister verarbeiten Daten ausschließlich im Rahmen der Auftragsverarbeitung.", + PrivacyPolicyUpdatedAt = new DateTimeOffset(new DateTime(2026, 6, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + PrivacyPolicyUpdatedBy = "seed", + RiskRulesJson = "[{\"key\":\"resubmitted_ballot\",\"label\":\"Ballot erneut gespeichert\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"low\",\"description\":\"Erstellt einen Hinweis, wenn ein User sein Voting erneut speichert.\"},{\"key\":\"rapid_vote_updates\",\"label\":\"Voting-Burst\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr schnell mehrere Voting-Aenderungen ausloest.\"},{\"key\":\"resubmitted_nomination\",\"label\":\"Nominierung erneut eingereicht\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"low\",\"description\":\"Erstellt einen Hinweis, wenn ein User in derselben Kategorie erneut nominiert.\"},{\"key\":\"rapid_nomination_burst\",\"label\":\"Nominierungs-Burst\",\"enabled\":true,\"threshold\":10,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr viele Nominierungen in kurzer Zeit sendet.\"},{\"key\":\"duplicate_clip_submission\",\"label\":\"Doppelter Clip\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn ein User denselben Clip erneut einreicht.\"},{\"key\":\"rapid_clip_burst\",\"label\":\"Clip-Burst\",\"enabled\":true,\"threshold\":5,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr viele Clips in kurzer Zeit sendet.\"},{\"key\":\"rapid_login_ip\",\"label\":\"Login-Burst pro IP\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":15,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn mehrere neue Sessions von derselben IP entstehen.\"},{\"key\":\"rapid_demo_login_ip\",\"label\":\"Demo-Login-Burst pro IP\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":15,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn mehrere Demo-Admin-Sessions von derselben IP entstehen.\"}]", + SocialLinksJson = "[{\"label\":\"Twitch\",\"platform\":\"twitch\",\"url\":\"https://twitch.tv/jayuhime\",\"icon\":\"twitch\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"YouTube\",\"platform\":\"youtube\",\"url\":\"https://youtube.com/c/Jayuhime\",\"icon\":\"youtube\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"X\",\"platform\":\"x\",\"url\":\"https://x.com/jayuhime\",\"icon\":\"x\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Instagram\",\"platform\":\"instagram\",\"url\":\"https://instagram.com/jayuhime\",\"icon\":\"instagram\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Discord\",\"platform\":\"discord\",\"url\":\"https://discord.gg/jayuhime\",\"icon\":\"discord\",\"showOnHost\":true,\"showOnCommunity\":true}]", + SponsorsContent = "Sponsoren & Partner\nHier koennen Sponsor:innen, Medienpartner, Community-Partner und Supporter des VTuber Star Awards vorgestellt werden.\n\nPartner werden im Rahmen der Show und auf den oeffentlichen Kontaktflaechen genannt, sobald sie final bestaetigt sind.", + SponsorsUrl = "https://vtuber-star-awards.de/partner", + TwitchAuthManagedByDatabase = false, + TwitchClientId = "", + TwitchClientSecret = "", + TwitchRedirectUri = "", + TwitchScope = "", + WorkflowRulesJson = "[{\"key\":\"max_finalists_per_category\",\"label\":\"Finale Kandidat:innen pro Kategorie\",\"enabled\":true,\"limit\":4,\"mode\":\"block\",\"description\":\"Begrenzt, wie viele finale Kandidat:innen in einer Kategorie vorbereitet werden.\"},{\"key\":\"max_candidate_appearances\",\"label\":\"Kandidaturen pro Person\",\"enabled\":true,\"limit\":2,\"mode\":\"warn\",\"description\":\"Warnt oder blockiert, wenn dieselbe Person in zu vielen Kategorien final auftaucht.\"},{\"key\":\"max_winner_placements\",\"label\":\"Gewinnerpl\\u00E4tze pro Person\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn dieselbe Person mehrfach als Gewinner:in gesetzt wird.\"}]" + }); + }); + + modelBuilder.Entity("Backend.Domain.TeamMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoundTwitchDisplayName") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("BoundTwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Login") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MustChangePassword") + .HasColumnType("boolean"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("PasswordResetAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("TwitchBoundAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("BoundTwitchUserId") + .IsUnique(); + + b.HasIndex("Login") + .IsUnique(); + + b.ToTable("TeamMembers"); + }); + + modelBuilder.Entity("Backend.Domain.TeamRolePermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PermissionsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("Role") + .IsUnique(); + + b.ToTable("TeamRolePermissions"); + }); + + modelBuilder.Entity("Backend.Domain.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SessionToken") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SessionToken") + .IsUnique(); + + b.ToTable("UserSessions"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "SubmittedByTwitchId") + .IsUnique(); + + b.ToTable("VoteBallots"); + + b.HasData( + new + { + Id = 1, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_1" + }, + new + { + Id = 2, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 5, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_2" + }); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BallotId") + .HasColumnType("integer"); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BallotId"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.ToTable("VoteEntries"); + + b.HasData( + new + { + Id = 1, + BallotId = 1, + CandidateId = 1, + CategoryId = 1 + }, + new + { + Id = 2, + BallotId = 1, + CandidateId = 4, + CategoryId = 2 + }, + new + { + Id = 3, + BallotId = 2, + CandidateId = 2, + CategoryId = 1 + }, + new + { + Id = 4, + BallotId = 2, + CandidateId = 6, + CategoryId = 3 + }); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Results") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.HasOne("Backend.Domain.Category", "Category") + .WithMany("Candidates") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Categories") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Candidate"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId"); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.HasOne("Backend.Domain.VoteBallot", "Ballot") + .WithMany("Entries") + .HasForeignKey("BallotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ballot"); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Navigation("Candidates"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Navigation("Categories"); + + b.Navigation("Results"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Navigation("Entries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/Migrations/20260627125128_AddWorkflowRulesJson.cs b/Backend/Migrations/20260627125128_AddWorkflowRulesJson.cs new file mode 100644 index 0000000..d638ed6 --- /dev/null +++ b/Backend/Migrations/20260627125128_AddWorkflowRulesJson.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backend.Migrations +{ + /// + public partial class AddWorkflowRulesJson : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(""" + ALTER TABLE "SiteSettings" + ADD COLUMN IF NOT EXISTS "WorkflowRulesJson" text NOT NULL DEFAULT '[]'; + """); + + migrationBuilder.UpdateData( + table: "SiteSettings", + keyColumn: "Id", + keyValue: 1, + column: "WorkflowRulesJson", + value: "[{\"key\":\"max_finalists_per_category\",\"label\":\"Finale Kandidat:innen pro Kategorie\",\"enabled\":true,\"limit\":4,\"mode\":\"block\",\"description\":\"Begrenzt, wie viele finale Kandidat:innen in einer Kategorie vorbereitet werden.\"},{\"key\":\"max_candidate_appearances\",\"label\":\"Kandidaturen pro Person\",\"enabled\":true,\"limit\":2,\"mode\":\"warn\",\"description\":\"Warnt oder blockiert, wenn dieselbe Person in zu vielen Kategorien final auftaucht.\"},{\"key\":\"max_winner_placements\",\"label\":\"Gewinnerpl\\u00E4tze pro Person\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn dieselbe Person mehrfach als Gewinner:in gesetzt wird.\"}]"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(""" + ALTER TABLE "SiteSettings" + DROP COLUMN IF EXISTS "WorkflowRulesJson"; + """); + } + } +} diff --git a/Backend/Migrations/20260627131746_AddOptionalClipFeatureSettings.Designer.cs b/Backend/Migrations/20260627131746_AddOptionalClipFeatureSettings.Designer.cs new file mode 100644 index 0000000..0d188de --- /dev/null +++ b/Backend/Migrations/20260627131746_AddOptionalClipFeatureSettings.Designer.cs @@ -0,0 +1,1552 @@ +// +using System; +using Backend.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backend.Migrations +{ + [DbContext(typeof(AwardsDbContext))] + [Migration("20260627131746_AddOptionalClipFeatureSettings")] + partial class AddOptionalClipFeatureSettings + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backend.Domain.AdminAuditEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("AdminTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.ToTable("AdminAuditEntries"); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CategoryName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "CategoryId") + .IsUnique(); + + b.ToTable("Results"); + + b.HasData( + new + { + Id = 1, + CandidateId = 8, + CategoryId = 5, + CategoryName = "VTuber des Jahres", + SeasonId = 2 + }, + new + { + Id = 2, + CandidateId = 9, + CategoryId = 6, + CategoryName = "Bestes Live Event", + SeasonId = 2 + }, + new + { + Id = 3, + CandidateId = 10, + CategoryId = 7, + CategoryName = "Clip des Jahres", + SeasonId = 2 + }, + new + { + Id = 4, + CandidateId = 11, + CategoryId = 8, + CategoryName = "VTuber des Jahres", + SeasonId = 3 + }, + new + { + Id = 5, + CandidateId = 12, + CategoryId = 9, + CategoryName = "Clip des Jahres", + SeasonId = 3 + }, + new + { + Id = 6, + CandidateId = 13, + CategoryId = 10, + CategoryName = "VTuber des Jahres", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AcceptanceNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AcceptanceStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasDefaultValue("open"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ChannelSlug") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ClipCompilationPlatform") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ClipCompilationTitle") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ClipCompilationUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ClipEmbedStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasDefaultValue("unchecked"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId"); + + b.ToTable("Candidates"); + + b.HasData( + new + { + Id = 1, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@hoshimimiyu", + ClipEmbedStatus = "unchecked", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 2, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 3, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@shiroch", + ClipEmbedStatus = "unchecked", + DisplayName = "Shiro Ch.", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 4, + AcceptanceStatus = "open", + CategoryId = 2, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 5, + AcceptanceStatus = "open", + CategoryId = 2, + ChannelSlug = "@aoisakura", + ClipEmbedStatus = "unchecked", + DisplayName = "Aoi Sakura Showcase", + Platform = "YouTube", + SeasonId = 1 + }, + new + { + Id = 6, + AcceptanceStatus = "open", + CategoryId = 3, + ChannelSlug = "@pyonkichikingdom", + ClipEmbedStatus = "unchecked", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 7, + AcceptanceStatus = "open", + CategoryId = 4, + ChannelSlug = "@moonrelay", + ClipEmbedStatus = "unchecked", + DisplayName = "Moonrelay", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 8, + AcceptanceStatus = "open", + CategoryId = 5, + ChannelSlug = "@hoshimimiyu", + ClipEmbedStatus = "unchecked", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 9, + AcceptanceStatus = "open", + CategoryId = 6, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 10, + AcceptanceStatus = "open", + CategoryId = 7, + ChannelSlug = "@pyonkichikingdom", + ClipEmbedStatus = "unchecked", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 11, + AcceptanceStatus = "open", + CategoryId = 8, + ChannelSlug = "@aoisakura", + ClipEmbedStatus = "unchecked", + DisplayName = "Aoi Sakura", + Platform = "YouTube", + SeasonId = 3 + }, + new + { + Id = 12, + AcceptanceStatus = "open", + CategoryId = 9, + ChannelSlug = "@starbyte", + ClipEmbedStatus = "unchecked", + DisplayName = "Starbyte", + Platform = "Twitch", + SeasonId = 3 + }, + new + { + Id = 13, + AcceptanceStatus = "open", + CategoryId = 10, + ChannelSlug = "@tenshivox", + ClipEmbedStatus = "unchecked", + DisplayName = "Tenshi Vox", + Platform = "Twitch", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MaxNomineesPerUser") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Slug") + .IsUnique(); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = 1, + Description = "Die groesste Auszeichnung des Jahres.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 1, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 2, + Description = "Events, Konzerte und 3D-Shows.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 1, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 3, + Description = "Der lustigste oder emotionalste Clip des Jahres.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 1, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 4, + Description = "Die aktivste und freundlichste Community.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "Beste Community", + SeasonId = 1, + Slug = "beste-community", + SortOrder = 4 + }, + new + { + Id = 5, + Description = "Archivkategorie 2025.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 2, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 6, + Description = "Archivkategorie 2025.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 2, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 7, + Description = "Archivkategorie 2025.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 2, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 8, + Description = "Archivkategorie 2024.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 3, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 9, + Description = "Archivkategorie 2024.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 3, + Slug = "clip-des-jahres", + SortOrder = 2 + }, + new + { + Id = 10, + Description = "Archivkategorie 2023.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 4, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ClipUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Creator") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("ClipSubmissions"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CandidateText") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("StreamUrl") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("Nominations"); + + b.HasData( + new + { + Id = 1, + CandidateText = "Hoshimi Miyu", + CategoryId = 1, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 13, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_hoshi" + }, + new + { + Id = 2, + CandidateText = "Kurainu 3D Live", + CategoryId = 2, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 14, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_kurainu" + }); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("TwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("RiskFlags"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrentPhase") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("IsCommunityOnly") + .HasColumnType("boolean"); + + b.Property("IsCurrent") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("NominationEndsAt") + .HasColumnType("date"); + + b.Property("NominationStartsAt") + .HasColumnType("date"); + + b.Property("ReviewEndsAt") + .HasColumnType("date"); + + b.Property("ReviewStartsAt") + .HasColumnType("date"); + + b.Property("ShowDate") + .HasColumnType("date"); + + b.Property("ShowStartsAt") + .HasColumnType("time without time zone"); + + b.Property("ShowStreamUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("VotingEndsAt") + .HasColumnType("date"); + + b.Property("VotingStartsAt") + .HasColumnType("date"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("Seasons"); + + b.HasData( + new + { + Id = 1, + CurrentPhase = "Community Voting", + IsCommunityOnly = true, + IsCurrent = true, + Name = "VTuber Star Awards 2026", + NominationEndsAt = new DateOnly(2026, 5, 31), + NominationStartsAt = new DateOnly(2026, 5, 1), + ReviewEndsAt = new DateOnly(2026, 7, 10), + ReviewStartsAt = new DateOnly(2026, 7, 1), + ShowDate = new DateOnly(2026, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2026, 6, 30), + VotingStartsAt = new DateOnly(2026, 6, 1), + Year = 2026 + }, + new + { + Id = 2, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2025", + NominationEndsAt = new DateOnly(2025, 5, 31), + NominationStartsAt = new DateOnly(2025, 5, 1), + ReviewEndsAt = new DateOnly(2025, 7, 10), + ReviewStartsAt = new DateOnly(2025, 7, 1), + ShowDate = new DateOnly(2025, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2025, 6, 30), + VotingStartsAt = new DateOnly(2025, 6, 1), + Year = 2025 + }, + new + { + Id = 3, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2024", + NominationEndsAt = new DateOnly(2024, 5, 31), + NominationStartsAt = new DateOnly(2024, 5, 1), + ReviewEndsAt = new DateOnly(2024, 7, 10), + ReviewStartsAt = new DateOnly(2024, 7, 1), + ShowDate = new DateOnly(2024, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://youtube.com/c/Jayuhime", + VotingEndsAt = new DateOnly(2024, 6, 30), + VotingStartsAt = new DateOnly(2024, 6, 1), + Year = 2024 + }, + new + { + Id = 4, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2023", + NominationEndsAt = new DateOnly(2023, 5, 31), + NominationStartsAt = new DateOnly(2023, 5, 1), + ReviewEndsAt = new DateOnly(2023, 7, 10), + ReviewStartsAt = new DateOnly(2023, 7, 1), + ShowDate = new DateOnly(2023, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2023, 6, 30), + VotingStartsAt = new DateOnly(2023, 6, 1), + Year = 2023 + }); + }); + + modelBuilder.Entity("Backend.Domain.SiteSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClipReviewEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ClipSubmissionDisabledMessage") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("ClipSubmissionsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ContactContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("DemoLoginDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginEmail") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("DemoLoginEnabled") + .HasColumnType("boolean"); + + b.Property("DemoLoginManagedByDatabase") + .HasColumnType("boolean"); + + b.Property("DemoLoginPasswordHash") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginPasswordSalt") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DemoLoginTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("FaqJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("HostDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("HostTagline") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("ImprintContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("ImprintUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("MaintenanceMessage") + .IsRequired() + .HasMaxLength(600) + .HasColumnType("character varying(600)"); + + b.Property("MaintenanceModeEnabled") + .HasColumnType("boolean"); + + b.Property("MaintenanceTitle") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("NewsletterUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("PrivacyEmail") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("PrivacyPolicyContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("PrivacyPolicyUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivacyPolicyUpdatedBy") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("RiskRulesJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SocialLinksJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("TwitchAuthManagedByDatabase") + .HasColumnType("boolean"); + + b.Property("TwitchClientId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchClientSecret") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("TwitchRedirectUri") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("TwitchScope") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("WorkflowRulesJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("[]"); + + b.HasKey("Id"); + + b.ToTable("SiteSettings"); + + b.HasData( + new + { + Id = 1, + ClipReviewEnabled = true, + ClipSubmissionDisabledMessage = "Clip-Einreichungen sind aktuell geschlossen.", + ClipSubmissionsEnabled = false, + ContactContent = "Kontakt zum Award-Team\nDu hast Fragen zur Nominierung, zum Voting, zu Clips oder zur Show? Schreib dem Team ueber die hinterlegte Kontaktseite.\n\nDatenschutzfragen\nFuer Datenschutzanfragen nutze bitte die Datenschutz-E-Mail aus diesem Footer.\n\nCommunity & Kooperationen\nSocial Links und Partneranfragen werden vom Admin-Team gepflegt und koordiniert.", + ContactUrl = "https://vtuber-star-awards.de/kontakt", + DemoLoginDisplayName = "Jayuhime Admin", + DemoLoginEmail = "", + DemoLoginEnabled = false, + DemoLoginManagedByDatabase = false, + DemoLoginPasswordHash = "", + DemoLoginPasswordSalt = "", + DemoLoginTwitchUserId = "jayuhime_admin", + FaqJson = "[{\"question\":\"Wer darf nominiert werden?\",\"answer\":\"Jede:r aktive deutschsprachige VTuber kann nominiert werden \\u2014 unabh\\u00E4ngig von Follower-Zahl oder Plattform. Die Community schl\\u00E4gt in der Nominierungsphase ihre Favorit:innen vor.\"},{\"question\":\"Wie funktioniert das Voting?\",\"answer\":\"Du meldest dich ausschlie\\u00DFlich mit deinem Twitch-Account an \\u2014 nur so kannst du teilnehmen. Das h\\u00E4lt das Voting fair und bot-frei. Danach vergibst du pro Kategorie eine Stimme, \\u00E4nderbar bis zum Ende der Phase.\"},{\"question\":\"Was kostet die Teilnahme?\",\"answer\":\"Gar nichts. Nominieren, voten und die Show schauen ist komplett kostenlos \\u2014 der VTuber Star Award ist ein Community-Event von Fans f\\u00FCr Fans.\"},{\"question\":\"Wann und wo findet die Award-Show statt?\",\"answer\":\"Die gro\\u00DFe Live-Show wird von Jayuhime gehostet und auf Twitch \\u0026 YouTube gestreamt. Den genauen Termin findest du im Countdown oben \\u2014 sei live dabei, wenn die Stars gek\\u00FCrt werden!\"},{\"question\":\"Ich wurde nominiert \\u2014 was nun?\",\"answer\":\"Gl\\u00FCckwunsch! Du erh\\u00E4ltst eine Benachrichtigung mit allen Infos. Teile deine Nominierung gern mit deiner Community und ruf zum Voten auf \\u2014 jede Stimme z\\u00E4hlt.\"}]", + HostDisplayName = "Jayuhime", + HostTagline = "VTuber & Award Host", + ImprintContent = "Anbieter\nVTuber Star Awards, vertreten durch Jayuhime.\n\nKontakt\nNutze fuer organisatorische Fragen bitte die Kontaktseite oder die hinterlegte Kontaktadresse.\n\nHinweis\nDieses Impressum ist ein redaktioneller Platzhalter und sollte vor dem Livegang mit den tatsaechlichen Anbieterangaben ersetzt werden.", + ImprintUrl = "https://vtuber-star-awards.de/impressum", + MaintenanceMessage = "Die Award-Galaxie wird gerade liebevoll poliert. Schau gleich wieder vorbei.", + MaintenanceModeEnabled = false, + MaintenanceTitle = "Sternenpause", + NewsletterUrl = "https://vtuber-star-awards.de/newsletter", + PrivacyEmail = "datenschutz@vtuber-star-awards.de", + PrivacyPolicyContent = "Verantwortliche:r\nVTuber Star Awards, vertreten durch Jayuhime. Kontakt: datenschutz@vtuber-star-awards.de\n\nWelche Daten wir verarbeiten\nBei Teilnahme (Voting, Nominierung, Clip-Einreichung) verarbeiten wir ausschließlich deine Twitch-User-ID sowie den Zeitstempel deiner Aktion.\nFür Show-Erinnerungen speichern wir optional deine E-Mail-Adresse.\n\nRechtsgrundlage\nVerarbeitung auf Basis von Art. 6 Abs. 1 lit. b DSGVO. Für optionale Erinnerungen gilt Art. 6 Abs. 1 lit. a DSGVO.\n\nZweck der Verarbeitung\nDurchführung des VTuber Star Awards, Sicherstellung fairer Abstimmung, Spam-Prävention sowie Admin-Review von Einreichungen.\n\nLöschfristen\nAlle Teilnahmedaten werden spätestens 6 Monate nach der Award-Show automatisch gelöscht. E-Mail-Adressen werden nach der Show entfernt.\n\nDeine Rechte\nDu hast das Recht auf Auskunft, Löschung, Berichtigung, Widerspruch und Beschwerde bei der zuständigen Aufsichtsbehörde.\n\nWeitergabe an Dritte\nKeine Weitergabe zu Werbezwecken. Technische Dienstleister verarbeiten Daten ausschließlich im Rahmen der Auftragsverarbeitung.", + PrivacyPolicyUpdatedAt = new DateTimeOffset(new DateTime(2026, 6, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + PrivacyPolicyUpdatedBy = "seed", + RiskRulesJson = "[{\"key\":\"resubmitted_ballot\",\"label\":\"Ballot erneut gespeichert\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"low\",\"description\":\"Erstellt einen Hinweis, wenn ein User sein Voting erneut speichert.\"},{\"key\":\"rapid_vote_updates\",\"label\":\"Voting-Burst\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr schnell mehrere Voting-Aenderungen ausloest.\"},{\"key\":\"resubmitted_nomination\",\"label\":\"Nominierung erneut eingereicht\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"low\",\"description\":\"Erstellt einen Hinweis, wenn ein User in derselben Kategorie erneut nominiert.\"},{\"key\":\"rapid_nomination_burst\",\"label\":\"Nominierungs-Burst\",\"enabled\":true,\"threshold\":10,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr viele Nominierungen in kurzer Zeit sendet.\"},{\"key\":\"duplicate_clip_submission\",\"label\":\"Doppelter Clip\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn ein User denselben Clip erneut einreicht.\"},{\"key\":\"rapid_clip_burst\",\"label\":\"Clip-Burst\",\"enabled\":true,\"threshold\":5,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr viele Clips in kurzer Zeit sendet.\"},{\"key\":\"rapid_login_ip\",\"label\":\"Login-Burst pro IP\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":15,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn mehrere neue Sessions von derselben IP entstehen.\"},{\"key\":\"rapid_demo_login_ip\",\"label\":\"Demo-Login-Burst pro IP\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":15,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn mehrere Demo-Admin-Sessions von derselben IP entstehen.\"}]", + SocialLinksJson = "[{\"label\":\"Twitch\",\"platform\":\"twitch\",\"url\":\"https://twitch.tv/jayuhime\",\"icon\":\"twitch\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"YouTube\",\"platform\":\"youtube\",\"url\":\"https://youtube.com/c/Jayuhime\",\"icon\":\"youtube\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"X\",\"platform\":\"x\",\"url\":\"https://x.com/jayuhime\",\"icon\":\"x\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Instagram\",\"platform\":\"instagram\",\"url\":\"https://instagram.com/jayuhime\",\"icon\":\"instagram\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Discord\",\"platform\":\"discord\",\"url\":\"https://discord.gg/jayuhime\",\"icon\":\"discord\",\"showOnHost\":true,\"showOnCommunity\":true}]", + SponsorsContent = "Sponsoren & Partner\nHier koennen Sponsor:innen, Medienpartner, Community-Partner und Supporter des VTuber Star Awards vorgestellt werden.\n\nPartner werden im Rahmen der Show und auf den oeffentlichen Kontaktflaechen genannt, sobald sie final bestaetigt sind.", + SponsorsUrl = "https://vtuber-star-awards.de/partner", + TwitchAuthManagedByDatabase = false, + TwitchClientId = "", + TwitchClientSecret = "", + TwitchRedirectUri = "", + TwitchScope = "", + WorkflowRulesJson = "[{\"key\":\"max_finalists_per_category\",\"label\":\"Finale Kandidat:innen pro Kategorie\",\"enabled\":true,\"limit\":4,\"mode\":\"block\",\"description\":\"Begrenzt, wie viele finale Kandidat:innen in einer Kategorie vorbereitet werden.\"},{\"key\":\"max_candidate_appearances\",\"label\":\"Kandidaturen pro Person\",\"enabled\":true,\"limit\":2,\"mode\":\"warn\",\"description\":\"Warnt oder blockiert, wenn dieselbe Person in zu vielen Kategorien final auftaucht.\"},{\"key\":\"max_winner_placements\",\"label\":\"Gewinnerpl\\u00E4tze pro Person\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn dieselbe Person mehrfach als Gewinner:in gesetzt wird.\"}]" + }); + }); + + modelBuilder.Entity("Backend.Domain.TeamMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoundTwitchDisplayName") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("BoundTwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Login") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MustChangePassword") + .HasColumnType("boolean"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("PasswordResetAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("TwitchBoundAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("BoundTwitchUserId") + .IsUnique(); + + b.HasIndex("Login") + .IsUnique(); + + b.ToTable("TeamMembers"); + }); + + modelBuilder.Entity("Backend.Domain.TeamRolePermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PermissionsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("Role") + .IsUnique(); + + b.ToTable("TeamRolePermissions"); + }); + + modelBuilder.Entity("Backend.Domain.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SessionToken") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SessionToken") + .IsUnique(); + + b.ToTable("UserSessions"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "SubmittedByTwitchId") + .IsUnique(); + + b.ToTable("VoteBallots"); + + b.HasData( + new + { + Id = 1, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_1" + }, + new + { + Id = 2, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 5, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_2" + }); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BallotId") + .HasColumnType("integer"); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BallotId"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.ToTable("VoteEntries"); + + b.HasData( + new + { + Id = 1, + BallotId = 1, + CandidateId = 1, + CategoryId = 1 + }, + new + { + Id = 2, + BallotId = 1, + CandidateId = 4, + CategoryId = 2 + }, + new + { + Id = 3, + BallotId = 2, + CandidateId = 2, + CategoryId = 1 + }, + new + { + Id = 4, + BallotId = 2, + CandidateId = 6, + CategoryId = 3 + }); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Results") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.HasOne("Backend.Domain.Category", "Category") + .WithMany("Candidates") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Categories") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Candidate"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId"); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.HasOne("Backend.Domain.VoteBallot", "Ballot") + .WithMany("Entries") + .HasForeignKey("BallotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ballot"); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Navigation("Candidates"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Navigation("Categories"); + + b.Navigation("Results"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Navigation("Entries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/Migrations/20260627131746_AddOptionalClipFeatureSettings.cs b/Backend/Migrations/20260627131746_AddOptionalClipFeatureSettings.cs new file mode 100644 index 0000000..cfd72a2 --- /dev/null +++ b/Backend/Migrations/20260627131746_AddOptionalClipFeatureSettings.cs @@ -0,0 +1,55 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backend.Migrations +{ + /// + public partial class AddOptionalClipFeatureSettings : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(""" + ALTER TABLE "SiteSettings" + ADD COLUMN IF NOT EXISTS "ClipReviewEnabled" boolean NOT NULL DEFAULT true; + """); + + migrationBuilder.Sql(""" + ALTER TABLE "SiteSettings" + ADD COLUMN IF NOT EXISTS "ClipSubmissionDisabledMessage" character varying(240) NOT NULL DEFAULT 'Clip-Einreichungen sind aktuell geschlossen.'; + """); + + migrationBuilder.Sql(""" + ALTER TABLE "SiteSettings" + ADD COLUMN IF NOT EXISTS "ClipSubmissionsEnabled" boolean NOT NULL DEFAULT false; + """); + + migrationBuilder.UpdateData( + table: "SiteSettings", + keyColumn: "Id", + keyValue: 1, + columns: new[] { "ClipReviewEnabled", "ClipSubmissionDisabledMessage" }, + values: new object[] { true, "Clip-Einreichungen sind aktuell geschlossen." }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(""" + ALTER TABLE "SiteSettings" + DROP COLUMN IF EXISTS "ClipReviewEnabled"; + """); + + migrationBuilder.Sql(""" + ALTER TABLE "SiteSettings" + DROP COLUMN IF EXISTS "ClipSubmissionDisabledMessage"; + """); + + migrationBuilder.Sql(""" + ALTER TABLE "SiteSettings" + DROP COLUMN IF EXISTS "ClipSubmissionsEnabled"; + """); + } + } +} diff --git a/Backend/Migrations/20260627135729_AddNominationLinkBlacklist.Designer.cs b/Backend/Migrations/20260627135729_AddNominationLinkBlacklist.Designer.cs new file mode 100644 index 0000000..49cb260 --- /dev/null +++ b/Backend/Migrations/20260627135729_AddNominationLinkBlacklist.Designer.cs @@ -0,0 +1,1559 @@ +// +using System; +using Backend.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backend.Migrations +{ + [DbContext(typeof(AwardsDbContext))] + [Migration("20260627135729_AddNominationLinkBlacklist")] + partial class AddNominationLinkBlacklist + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backend.Domain.AdminAuditEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("AdminTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.ToTable("AdminAuditEntries"); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CategoryName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "CategoryId") + .IsUnique(); + + b.ToTable("Results"); + + b.HasData( + new + { + Id = 1, + CandidateId = 8, + CategoryId = 5, + CategoryName = "VTuber des Jahres", + SeasonId = 2 + }, + new + { + Id = 2, + CandidateId = 9, + CategoryId = 6, + CategoryName = "Bestes Live Event", + SeasonId = 2 + }, + new + { + Id = 3, + CandidateId = 10, + CategoryId = 7, + CategoryName = "Clip des Jahres", + SeasonId = 2 + }, + new + { + Id = 4, + CandidateId = 11, + CategoryId = 8, + CategoryName = "VTuber des Jahres", + SeasonId = 3 + }, + new + { + Id = 5, + CandidateId = 12, + CategoryId = 9, + CategoryName = "Clip des Jahres", + SeasonId = 3 + }, + new + { + Id = 6, + CandidateId = 13, + CategoryId = 10, + CategoryName = "VTuber des Jahres", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AcceptanceNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AcceptanceStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasDefaultValue("open"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ChannelSlug") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ClipCompilationPlatform") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ClipCompilationTitle") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ClipCompilationUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ClipEmbedStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasDefaultValue("unchecked"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId"); + + b.ToTable("Candidates"); + + b.HasData( + new + { + Id = 1, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@hoshimimiyu", + ClipEmbedStatus = "unchecked", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 2, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 3, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@shiroch", + ClipEmbedStatus = "unchecked", + DisplayName = "Shiro Ch.", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 4, + AcceptanceStatus = "open", + CategoryId = 2, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 5, + AcceptanceStatus = "open", + CategoryId = 2, + ChannelSlug = "@aoisakura", + ClipEmbedStatus = "unchecked", + DisplayName = "Aoi Sakura Showcase", + Platform = "YouTube", + SeasonId = 1 + }, + new + { + Id = 6, + AcceptanceStatus = "open", + CategoryId = 3, + ChannelSlug = "@pyonkichikingdom", + ClipEmbedStatus = "unchecked", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 7, + AcceptanceStatus = "open", + CategoryId = 4, + ChannelSlug = "@moonrelay", + ClipEmbedStatus = "unchecked", + DisplayName = "Moonrelay", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 8, + AcceptanceStatus = "open", + CategoryId = 5, + ChannelSlug = "@hoshimimiyu", + ClipEmbedStatus = "unchecked", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 9, + AcceptanceStatus = "open", + CategoryId = 6, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 10, + AcceptanceStatus = "open", + CategoryId = 7, + ChannelSlug = "@pyonkichikingdom", + ClipEmbedStatus = "unchecked", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 11, + AcceptanceStatus = "open", + CategoryId = 8, + ChannelSlug = "@aoisakura", + ClipEmbedStatus = "unchecked", + DisplayName = "Aoi Sakura", + Platform = "YouTube", + SeasonId = 3 + }, + new + { + Id = 12, + AcceptanceStatus = "open", + CategoryId = 9, + ChannelSlug = "@starbyte", + ClipEmbedStatus = "unchecked", + DisplayName = "Starbyte", + Platform = "Twitch", + SeasonId = 3 + }, + new + { + Id = 13, + AcceptanceStatus = "open", + CategoryId = 10, + ChannelSlug = "@tenshivox", + ClipEmbedStatus = "unchecked", + DisplayName = "Tenshi Vox", + Platform = "Twitch", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MaxNomineesPerUser") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Slug") + .IsUnique(); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = 1, + Description = "Die groesste Auszeichnung des Jahres.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 1, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 2, + Description = "Events, Konzerte und 3D-Shows.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 1, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 3, + Description = "Der lustigste oder emotionalste Clip des Jahres.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 1, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 4, + Description = "Die aktivste und freundlichste Community.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "Beste Community", + SeasonId = 1, + Slug = "beste-community", + SortOrder = 4 + }, + new + { + Id = 5, + Description = "Archivkategorie 2025.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 2, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 6, + Description = "Archivkategorie 2025.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 2, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 7, + Description = "Archivkategorie 2025.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 2, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 8, + Description = "Archivkategorie 2024.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 3, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 9, + Description = "Archivkategorie 2024.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 3, + Slug = "clip-des-jahres", + SortOrder = 2 + }, + new + { + Id = 10, + Description = "Archivkategorie 2023.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 4, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ClipUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Creator") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("ClipSubmissions"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CandidateText") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("StreamUrl") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("Nominations"); + + b.HasData( + new + { + Id = 1, + CandidateText = "Hoshimi Miyu", + CategoryId = 1, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 13, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_hoshi" + }, + new + { + Id = 2, + CandidateText = "Kurainu 3D Live", + CategoryId = 2, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 14, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_kurainu" + }); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("TwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("RiskFlags"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrentPhase") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("IsCommunityOnly") + .HasColumnType("boolean"); + + b.Property("IsCurrent") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("NominationEndsAt") + .HasColumnType("date"); + + b.Property("NominationStartsAt") + .HasColumnType("date"); + + b.Property("ReviewEndsAt") + .HasColumnType("date"); + + b.Property("ReviewStartsAt") + .HasColumnType("date"); + + b.Property("ShowDate") + .HasColumnType("date"); + + b.Property("ShowStartsAt") + .HasColumnType("time without time zone"); + + b.Property("ShowStreamUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("VotingEndsAt") + .HasColumnType("date"); + + b.Property("VotingStartsAt") + .HasColumnType("date"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("Seasons"); + + b.HasData( + new + { + Id = 1, + CurrentPhase = "Community Voting", + IsCommunityOnly = true, + IsCurrent = true, + Name = "VTuber Star Awards 2026", + NominationEndsAt = new DateOnly(2026, 5, 31), + NominationStartsAt = new DateOnly(2026, 5, 1), + ReviewEndsAt = new DateOnly(2026, 7, 10), + ReviewStartsAt = new DateOnly(2026, 7, 1), + ShowDate = new DateOnly(2026, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2026, 6, 30), + VotingStartsAt = new DateOnly(2026, 6, 1), + Year = 2026 + }, + new + { + Id = 2, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2025", + NominationEndsAt = new DateOnly(2025, 5, 31), + NominationStartsAt = new DateOnly(2025, 5, 1), + ReviewEndsAt = new DateOnly(2025, 7, 10), + ReviewStartsAt = new DateOnly(2025, 7, 1), + ShowDate = new DateOnly(2025, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2025, 6, 30), + VotingStartsAt = new DateOnly(2025, 6, 1), + Year = 2025 + }, + new + { + Id = 3, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2024", + NominationEndsAt = new DateOnly(2024, 5, 31), + NominationStartsAt = new DateOnly(2024, 5, 1), + ReviewEndsAt = new DateOnly(2024, 7, 10), + ReviewStartsAt = new DateOnly(2024, 7, 1), + ShowDate = new DateOnly(2024, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://youtube.com/c/Jayuhime", + VotingEndsAt = new DateOnly(2024, 6, 30), + VotingStartsAt = new DateOnly(2024, 6, 1), + Year = 2024 + }, + new + { + Id = 4, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2023", + NominationEndsAt = new DateOnly(2023, 5, 31), + NominationStartsAt = new DateOnly(2023, 5, 1), + ReviewEndsAt = new DateOnly(2023, 7, 10), + ReviewStartsAt = new DateOnly(2023, 7, 1), + ShowDate = new DateOnly(2023, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2023, 6, 30), + VotingStartsAt = new DateOnly(2023, 6, 1), + Year = 2023 + }); + }); + + modelBuilder.Entity("Backend.Domain.SiteSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClipReviewEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ClipSubmissionDisabledMessage") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("ClipSubmissionsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ContactContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("DemoLoginDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginEmail") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("DemoLoginEnabled") + .HasColumnType("boolean"); + + b.Property("DemoLoginManagedByDatabase") + .HasColumnType("boolean"); + + b.Property("DemoLoginPasswordHash") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginPasswordSalt") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DemoLoginTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("FaqJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("HostDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("HostTagline") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("ImprintContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("ImprintUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("MaintenanceMessage") + .IsRequired() + .HasMaxLength(600) + .HasColumnType("character varying(600)"); + + b.Property("MaintenanceModeEnabled") + .HasColumnType("boolean"); + + b.Property("MaintenanceTitle") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("NewsletterUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("NominationLinkBlacklistJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("[]"); + + b.Property("PrivacyEmail") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("PrivacyPolicyContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("PrivacyPolicyUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivacyPolicyUpdatedBy") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("RiskRulesJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SocialLinksJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("TwitchAuthManagedByDatabase") + .HasColumnType("boolean"); + + b.Property("TwitchClientId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchClientSecret") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("TwitchRedirectUri") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("TwitchScope") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("WorkflowRulesJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("[]"); + + b.HasKey("Id"); + + b.ToTable("SiteSettings"); + + b.HasData( + new + { + Id = 1, + ClipReviewEnabled = true, + ClipSubmissionDisabledMessage = "Clip-Einreichungen sind aktuell geschlossen.", + ClipSubmissionsEnabled = false, + ContactContent = "Kontakt zum Award-Team\nDu hast Fragen zur Nominierung, zum Voting, zu Clips oder zur Show? Schreib dem Team ueber die hinterlegte Kontaktseite.\n\nDatenschutzfragen\nFuer Datenschutzanfragen nutze bitte die Datenschutz-E-Mail aus diesem Footer.\n\nCommunity & Kooperationen\nSocial Links und Partneranfragen werden vom Admin-Team gepflegt und koordiniert.", + ContactUrl = "https://vtuber-star-awards.de/kontakt", + DemoLoginDisplayName = "Jayuhime Admin", + DemoLoginEmail = "", + DemoLoginEnabled = false, + DemoLoginManagedByDatabase = false, + DemoLoginPasswordHash = "", + DemoLoginPasswordSalt = "", + DemoLoginTwitchUserId = "jayuhime_admin", + FaqJson = "[{\"question\":\"Wer darf nominiert werden?\",\"answer\":\"Jede:r aktive deutschsprachige VTuber kann nominiert werden \\u2014 unabh\\u00E4ngig von Follower-Zahl oder Plattform. Die Community schl\\u00E4gt in der Nominierungsphase ihre Favorit:innen vor.\"},{\"question\":\"Wie funktioniert das Voting?\",\"answer\":\"Du meldest dich ausschlie\\u00DFlich mit deinem Twitch-Account an \\u2014 nur so kannst du teilnehmen. Das h\\u00E4lt das Voting fair und bot-frei. Danach vergibst du pro Kategorie eine Stimme, \\u00E4nderbar bis zum Ende der Phase.\"},{\"question\":\"Was kostet die Teilnahme?\",\"answer\":\"Gar nichts. Nominieren, voten und die Show schauen ist komplett kostenlos \\u2014 der VTuber Star Award ist ein Community-Event von Fans f\\u00FCr Fans.\"},{\"question\":\"Wann und wo findet die Award-Show statt?\",\"answer\":\"Die gro\\u00DFe Live-Show wird von Jayuhime gehostet und auf Twitch \\u0026 YouTube gestreamt. Den genauen Termin findest du im Countdown oben \\u2014 sei live dabei, wenn die Stars gek\\u00FCrt werden!\"},{\"question\":\"Ich wurde nominiert \\u2014 was nun?\",\"answer\":\"Gl\\u00FCckwunsch! Du erh\\u00E4ltst eine Benachrichtigung mit allen Infos. Teile deine Nominierung gern mit deiner Community und ruf zum Voten auf \\u2014 jede Stimme z\\u00E4hlt.\"}]", + HostDisplayName = "Jayuhime", + HostTagline = "VTuber & Award Host", + ImprintContent = "Anbieter\nVTuber Star Awards, vertreten durch Jayuhime.\n\nKontakt\nNutze fuer organisatorische Fragen bitte die Kontaktseite oder die hinterlegte Kontaktadresse.\n\nHinweis\nDieses Impressum ist ein redaktioneller Platzhalter und sollte vor dem Livegang mit den tatsaechlichen Anbieterangaben ersetzt werden.", + ImprintUrl = "https://vtuber-star-awards.de/impressum", + MaintenanceMessage = "Die Award-Galaxie wird gerade liebevoll poliert. Schau gleich wieder vorbei.", + MaintenanceModeEnabled = false, + MaintenanceTitle = "Sternenpause", + NewsletterUrl = "https://vtuber-star-awards.de/newsletter", + NominationLinkBlacklistJson = "[{\"Url\":\"https://kick.com/\"},{\"Url\":\"https://www.twitch.tv/\"},{\"Url\":\"https://www.youtube.com/\"}]", + PrivacyEmail = "datenschutz@vtuber-star-awards.de", + PrivacyPolicyContent = "Verantwortliche:r\nVTuber Star Awards, vertreten durch Jayuhime. Kontakt: datenschutz@vtuber-star-awards.de\n\nWelche Daten wir verarbeiten\nBei Teilnahme (Voting, Nominierung, Clip-Einreichung) verarbeiten wir ausschließlich deine Twitch-User-ID sowie den Zeitstempel deiner Aktion.\nFür Show-Erinnerungen speichern wir optional deine E-Mail-Adresse.\n\nRechtsgrundlage\nVerarbeitung auf Basis von Art. 6 Abs. 1 lit. b DSGVO. Für optionale Erinnerungen gilt Art. 6 Abs. 1 lit. a DSGVO.\n\nZweck der Verarbeitung\nDurchführung des VTuber Star Awards, Sicherstellung fairer Abstimmung, Spam-Prävention sowie Admin-Review von Einreichungen.\n\nLöschfristen\nAlle Teilnahmedaten werden spätestens 6 Monate nach der Award-Show automatisch gelöscht. E-Mail-Adressen werden nach der Show entfernt.\n\nDeine Rechte\nDu hast das Recht auf Auskunft, Löschung, Berichtigung, Widerspruch und Beschwerde bei der zuständigen Aufsichtsbehörde.\n\nWeitergabe an Dritte\nKeine Weitergabe zu Werbezwecken. Technische Dienstleister verarbeiten Daten ausschließlich im Rahmen der Auftragsverarbeitung.", + PrivacyPolicyUpdatedAt = new DateTimeOffset(new DateTime(2026, 6, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + PrivacyPolicyUpdatedBy = "seed", + RiskRulesJson = "[{\"key\":\"resubmitted_ballot\",\"label\":\"Ballot erneut gespeichert\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"low\",\"description\":\"Erstellt einen Hinweis, wenn ein User sein Voting erneut speichert.\"},{\"key\":\"rapid_vote_updates\",\"label\":\"Voting-Burst\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr schnell mehrere Voting-Aenderungen ausloest.\"},{\"key\":\"resubmitted_nomination\",\"label\":\"Nominierung erneut eingereicht\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"low\",\"description\":\"Erstellt einen Hinweis, wenn ein User in derselben Kategorie erneut nominiert.\"},{\"key\":\"rapid_nomination_burst\",\"label\":\"Nominierungs-Burst\",\"enabled\":true,\"threshold\":10,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr viele Nominierungen in kurzer Zeit sendet.\"},{\"key\":\"duplicate_clip_submission\",\"label\":\"Doppelter Clip\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn ein User denselben Clip erneut einreicht.\"},{\"key\":\"rapid_clip_burst\",\"label\":\"Clip-Burst\",\"enabled\":true,\"threshold\":5,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr viele Clips in kurzer Zeit sendet.\"},{\"key\":\"rapid_login_ip\",\"label\":\"Login-Burst pro IP\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":15,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn mehrere neue Sessions von derselben IP entstehen.\"},{\"key\":\"rapid_demo_login_ip\",\"label\":\"Demo-Login-Burst pro IP\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":15,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn mehrere Demo-Admin-Sessions von derselben IP entstehen.\"}]", + SocialLinksJson = "[{\"label\":\"Twitch\",\"platform\":\"twitch\",\"url\":\"https://twitch.tv/jayuhime\",\"icon\":\"twitch\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"YouTube\",\"platform\":\"youtube\",\"url\":\"https://youtube.com/c/Jayuhime\",\"icon\":\"youtube\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"X\",\"platform\":\"x\",\"url\":\"https://x.com/jayuhime\",\"icon\":\"x\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Instagram\",\"platform\":\"instagram\",\"url\":\"https://instagram.com/jayuhime\",\"icon\":\"instagram\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Discord\",\"platform\":\"discord\",\"url\":\"https://discord.gg/jayuhime\",\"icon\":\"discord\",\"showOnHost\":true,\"showOnCommunity\":true}]", + SponsorsContent = "Sponsoren & Partner\nHier koennen Sponsor:innen, Medienpartner, Community-Partner und Supporter des VTuber Star Awards vorgestellt werden.\n\nPartner werden im Rahmen der Show und auf den oeffentlichen Kontaktflaechen genannt, sobald sie final bestaetigt sind.", + SponsorsUrl = "https://vtuber-star-awards.de/partner", + TwitchAuthManagedByDatabase = false, + TwitchClientId = "", + TwitchClientSecret = "", + TwitchRedirectUri = "", + TwitchScope = "", + WorkflowRulesJson = "[{\"key\":\"max_finalists_per_category\",\"label\":\"Finale Kandidat:innen pro Kategorie\",\"enabled\":true,\"limit\":4,\"mode\":\"block\",\"description\":\"Begrenzt, wie viele finale Kandidat:innen in einer Kategorie vorbereitet werden.\"},{\"key\":\"max_candidate_appearances\",\"label\":\"Kandidaturen pro Person\",\"enabled\":true,\"limit\":2,\"mode\":\"warn\",\"description\":\"Warnt oder blockiert, wenn dieselbe Person in zu vielen Kategorien final auftaucht.\"},{\"key\":\"max_winner_placements\",\"label\":\"Gewinnerpl\\u00E4tze pro Person\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn dieselbe Person mehrfach als Gewinner:in gesetzt wird.\"}]" + }); + }); + + modelBuilder.Entity("Backend.Domain.TeamMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoundTwitchDisplayName") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("BoundTwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Login") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MustChangePassword") + .HasColumnType("boolean"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("PasswordResetAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("TwitchBoundAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("BoundTwitchUserId") + .IsUnique(); + + b.HasIndex("Login") + .IsUnique(); + + b.ToTable("TeamMembers"); + }); + + modelBuilder.Entity("Backend.Domain.TeamRolePermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PermissionsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("Role") + .IsUnique(); + + b.ToTable("TeamRolePermissions"); + }); + + modelBuilder.Entity("Backend.Domain.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SessionToken") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SessionToken") + .IsUnique(); + + b.ToTable("UserSessions"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "SubmittedByTwitchId") + .IsUnique(); + + b.ToTable("VoteBallots"); + + b.HasData( + new + { + Id = 1, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_1" + }, + new + { + Id = 2, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 5, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_2" + }); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BallotId") + .HasColumnType("integer"); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BallotId"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.ToTable("VoteEntries"); + + b.HasData( + new + { + Id = 1, + BallotId = 1, + CandidateId = 1, + CategoryId = 1 + }, + new + { + Id = 2, + BallotId = 1, + CandidateId = 4, + CategoryId = 2 + }, + new + { + Id = 3, + BallotId = 2, + CandidateId = 2, + CategoryId = 1 + }, + new + { + Id = 4, + BallotId = 2, + CandidateId = 6, + CategoryId = 3 + }); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Results") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.HasOne("Backend.Domain.Category", "Category") + .WithMany("Candidates") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Categories") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Candidate"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId"); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.HasOne("Backend.Domain.VoteBallot", "Ballot") + .WithMany("Entries") + .HasForeignKey("BallotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ballot"); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Navigation("Candidates"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Navigation("Categories"); + + b.Navigation("Results"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Navigation("Entries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/Migrations/20260627135729_AddNominationLinkBlacklist.cs b/Backend/Migrations/20260627135729_AddNominationLinkBlacklist.cs new file mode 100644 index 0000000..f67889f --- /dev/null +++ b/Backend/Migrations/20260627135729_AddNominationLinkBlacklist.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backend.Migrations +{ + /// + public partial class AddNominationLinkBlacklist : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "NominationLinkBlacklistJson", + table: "SiteSettings", + type: "text", + nullable: false, + defaultValue: "[]"); + + migrationBuilder.UpdateData( + table: "SiteSettings", + keyColumn: "Id", + keyValue: 1, + column: "NominationLinkBlacklistJson", + value: "[{\"Url\":\"https://kick.com/\"},{\"Url\":\"https://www.twitch.tv/\"},{\"Url\":\"https://www.youtube.com/\"}]"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "NominationLinkBlacklistJson", + table: "SiteSettings"); + } + } +} diff --git a/Backend/Migrations/20260627163650_AddClipAdminMenuVisible.Designer.cs b/Backend/Migrations/20260627163650_AddClipAdminMenuVisible.Designer.cs new file mode 100644 index 0000000..b31a5cd --- /dev/null +++ b/Backend/Migrations/20260627163650_AddClipAdminMenuVisible.Designer.cs @@ -0,0 +1,1758 @@ +// +using System; +using Backend.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backend.Migrations +{ + [DbContext(typeof(AwardsDbContext))] + [Migration("20260627163650_AddClipAdminMenuVisible")] + partial class AddClipAdminMenuVisible + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backend.Domain.AdminAuditEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("AdminTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.ToTable("AdminAuditEntries"); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CategoryName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "CategoryId") + .IsUnique(); + + b.ToTable("Results"); + + b.HasData( + new + { + Id = 1, + CandidateId = 8, + CategoryId = 5, + CategoryName = "VTuber des Jahres", + SeasonId = 2 + }, + new + { + Id = 2, + CandidateId = 9, + CategoryId = 6, + CategoryName = "Bestes Live Event", + SeasonId = 2 + }, + new + { + Id = 3, + CandidateId = 10, + CategoryId = 7, + CategoryName = "Clip des Jahres", + SeasonId = 2 + }, + new + { + Id = 4, + CandidateId = 11, + CategoryId = 8, + CategoryName = "VTuber des Jahres", + SeasonId = 3 + }, + new + { + Id = 5, + CandidateId = 12, + CategoryId = 9, + CategoryName = "Clip des Jahres", + SeasonId = 3 + }, + new + { + Id = 6, + CandidateId = 13, + CategoryId = 10, + CategoryName = "VTuber des Jahres", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AcceptanceNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AcceptanceStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasDefaultValue("open"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ChannelSlug") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ClipCompilationPlatform") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ClipCompilationTitle") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ClipCompilationUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ClipEmbedStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasDefaultValue("unchecked"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId"); + + b.ToTable("Candidates"); + + b.HasData( + new + { + Id = 1, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@hoshimimiyu", + ClipEmbedStatus = "unchecked", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 2, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 3, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@shiroch", + ClipEmbedStatus = "unchecked", + DisplayName = "Shiro Ch.", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 4, + AcceptanceStatus = "open", + CategoryId = 2, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 5, + AcceptanceStatus = "open", + CategoryId = 2, + ChannelSlug = "@aoisakura", + ClipEmbedStatus = "unchecked", + DisplayName = "Aoi Sakura Showcase", + Platform = "YouTube", + SeasonId = 1 + }, + new + { + Id = 6, + AcceptanceStatus = "open", + CategoryId = 3, + ChannelSlug = "@pyonkichikingdom", + ClipEmbedStatus = "unchecked", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 7, + AcceptanceStatus = "open", + CategoryId = 4, + ChannelSlug = "@moonrelay", + ClipEmbedStatus = "unchecked", + DisplayName = "Moonrelay", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 8, + AcceptanceStatus = "open", + CategoryId = 5, + ChannelSlug = "@hoshimimiyu", + ClipEmbedStatus = "unchecked", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 9, + AcceptanceStatus = "open", + CategoryId = 6, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 10, + AcceptanceStatus = "open", + CategoryId = 7, + ChannelSlug = "@pyonkichikingdom", + ClipEmbedStatus = "unchecked", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 11, + AcceptanceStatus = "open", + CategoryId = 8, + ChannelSlug = "@aoisakura", + ClipEmbedStatus = "unchecked", + DisplayName = "Aoi Sakura", + Platform = "YouTube", + SeasonId = 3 + }, + new + { + Id = 12, + AcceptanceStatus = "open", + CategoryId = 9, + ChannelSlug = "@starbyte", + ClipEmbedStatus = "unchecked", + DisplayName = "Starbyte", + Platform = "Twitch", + SeasonId = 3 + }, + new + { + Id = 13, + AcceptanceStatus = "open", + CategoryId = 10, + ChannelSlug = "@tenshivox", + ClipEmbedStatus = "unchecked", + DisplayName = "Tenshi Vox", + Platform = "Twitch", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MaxNomineesPerUser") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Slug") + .IsUnique(); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = 1, + Description = "Die groesste Auszeichnung des Jahres.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 1, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 2, + Description = "Events, Konzerte und 3D-Shows.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 1, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 3, + Description = "Der lustigste oder emotionalste Clip des Jahres.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 1, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 4, + Description = "Die aktivste und freundlichste Community.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "Beste Community", + SeasonId = 1, + Slug = "beste-community", + SortOrder = 4 + }, + new + { + Id = 5, + Description = "Archivkategorie 2025.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 2, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 6, + Description = "Archivkategorie 2025.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 2, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 7, + Description = "Archivkategorie 2025.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 2, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 8, + Description = "Archivkategorie 2024.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 3, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 9, + Description = "Archivkategorie 2024.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 3, + Slug = "clip-des-jahres", + SortOrder = 2 + }, + new + { + Id = 10, + Description = "Archivkategorie 2023.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 4, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ClipUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Creator") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("ClipSubmissions"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CandidateText") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("StreamUrl") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("Nominations"); + + b.HasData( + new + { + Id = 1, + CandidateText = "Hoshimi Miyu", + CategoryId = 1, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 13, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_hoshi" + }, + new + { + Id = 2, + CandidateText = "Kurainu 3D Live", + CategoryId = 2, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 14, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_kurainu" + }); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("TwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("RiskFlags"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrentPhase") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("IsCommunityOnly") + .HasColumnType("boolean"); + + b.Property("IsCurrent") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("NominationEndsAt") + .HasColumnType("date"); + + b.Property("NominationStartsAt") + .HasColumnType("date"); + + b.Property("ReviewEndsAt") + .HasColumnType("date"); + + b.Property("ReviewStartsAt") + .HasColumnType("date"); + + b.Property("ShowDate") + .HasColumnType("date"); + + b.Property("ShowStartsAt") + .HasColumnType("time without time zone"); + + b.Property("ShowStreamUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("VotingEndsAt") + .HasColumnType("date"); + + b.Property("VotingStartsAt") + .HasColumnType("date"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("Seasons"); + + b.HasData( + new + { + Id = 1, + CurrentPhase = "Community Voting", + IsCommunityOnly = true, + IsCurrent = true, + Name = "VTuber Star Awards 2026", + NominationEndsAt = new DateOnly(2026, 5, 31), + NominationStartsAt = new DateOnly(2026, 5, 1), + ReviewEndsAt = new DateOnly(2026, 7, 10), + ReviewStartsAt = new DateOnly(2026, 7, 1), + ShowDate = new DateOnly(2026, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2026, 6, 30), + VotingStartsAt = new DateOnly(2026, 6, 1), + Year = 2026 + }, + new + { + Id = 2, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2025", + NominationEndsAt = new DateOnly(2025, 5, 31), + NominationStartsAt = new DateOnly(2025, 5, 1), + ReviewEndsAt = new DateOnly(2025, 7, 10), + ReviewStartsAt = new DateOnly(2025, 7, 1), + ShowDate = new DateOnly(2025, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2025, 6, 30), + VotingStartsAt = new DateOnly(2025, 6, 1), + Year = 2025 + }, + new + { + Id = 3, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2024", + NominationEndsAt = new DateOnly(2024, 5, 31), + NominationStartsAt = new DateOnly(2024, 5, 1), + ReviewEndsAt = new DateOnly(2024, 7, 10), + ReviewStartsAt = new DateOnly(2024, 7, 1), + ShowDate = new DateOnly(2024, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://youtube.com/c/Jayuhime", + VotingEndsAt = new DateOnly(2024, 6, 30), + VotingStartsAt = new DateOnly(2024, 6, 1), + Year = 2024 + }, + new + { + Id = 4, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2023", + NominationEndsAt = new DateOnly(2023, 5, 31), + NominationStartsAt = new DateOnly(2023, 5, 1), + ReviewEndsAt = new DateOnly(2023, 7, 10), + ReviewStartsAt = new DateOnly(2023, 7, 1), + ShowDate = new DateOnly(2023, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2023, 6, 30), + VotingStartsAt = new DateOnly(2023, 6, 1), + Year = 2023 + }); + }); + + modelBuilder.Entity("Backend.Domain.ShowactApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ArtistName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ContactDiscord") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ContactEmail") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("PerformanceType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("PlatformUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReferenceUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TechnicalNotes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("ShowactApplications"); + }); + + modelBuilder.Entity("Backend.Domain.SiteSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClipAdminMenuVisible") + .HasColumnType("boolean"); + + b.Property("ClipReviewEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ClipSubmissionDisabledMessage") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("ClipSubmissionsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ContactContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("DemoLoginDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginEmail") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("DemoLoginEnabled") + .HasColumnType("boolean"); + + b.Property("DemoLoginManagedByDatabase") + .HasColumnType("boolean"); + + b.Property("DemoLoginPasswordHash") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginPasswordSalt") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DemoLoginTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("FaqJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("HostDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("HostTagline") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("ImprintContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("ImprintUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("MaintenanceMessage") + .IsRequired() + .HasMaxLength(600) + .HasColumnType("character varying(600)"); + + b.Property("MaintenanceModeEnabled") + .HasColumnType("boolean"); + + b.Property("MaintenanceTitle") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("NewsletterUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("NominationLinkBlacklistJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("[]"); + + b.Property("PrivacyEmail") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("PrivacyPolicyContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("PrivacyPolicyUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivacyPolicyUpdatedBy") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("RiskRulesJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShowactApplicationDisabledMessage") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("ShowactApplicationsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ShowactsContent") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue(""); + + b.Property("ShowactsUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("SocialLinksJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("SponsorsVisible") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("TwitchAuthManagedByDatabase") + .HasColumnType("boolean"); + + b.Property("TwitchClientId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchClientSecret") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("TwitchRedirectUri") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("TwitchScope") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("WorkflowRulesJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("[]"); + + b.HasKey("Id"); + + b.ToTable("SiteSettings"); + + b.HasData( + new + { + Id = 1, + ClipAdminMenuVisible = true, + ClipReviewEnabled = true, + ClipSubmissionDisabledMessage = "Clip-Einreichungen sind aktuell geschlossen.", + ClipSubmissionsEnabled = false, + ContactContent = "Kontakt zum Award-Team\nDu hast Fragen zur Nominierung, zum Voting, zu Clips oder zur Show? Schreib dem Team ueber die hinterlegte Kontaktseite.\n\nDatenschutzfragen\nFuer Datenschutzanfragen nutze bitte die Datenschutz-E-Mail aus diesem Footer.\n\nCommunity & Kooperationen\nSocial Links und Partneranfragen werden vom Admin-Team gepflegt und koordiniert.", + ContactUrl = "https://vtuber-star-awards.de/kontakt", + DemoLoginDisplayName = "Jayuhime Admin", + DemoLoginEmail = "", + DemoLoginEnabled = false, + DemoLoginManagedByDatabase = false, + DemoLoginPasswordHash = "", + DemoLoginPasswordSalt = "", + DemoLoginTwitchUserId = "jayuhime_admin", + FaqJson = "[{\"question\":\"Wer darf nominiert werden?\",\"answer\":\"Jede:r aktive deutschsprachige VTuber kann nominiert werden \\u2014 unabh\\u00E4ngig von Follower-Zahl oder Plattform. Die Community schl\\u00E4gt in der Nominierungsphase ihre Favorit:innen vor.\"},{\"question\":\"Wie funktioniert das Voting?\",\"answer\":\"Du meldest dich ausschlie\\u00DFlich mit deinem Twitch-Account an \\u2014 nur so kannst du teilnehmen. Das h\\u00E4lt das Voting fair und bot-frei. Danach vergibst du pro Kategorie eine Stimme, \\u00E4nderbar bis zum Ende der Phase.\"},{\"question\":\"Was kostet die Teilnahme?\",\"answer\":\"Gar nichts. Nominieren, voten und die Show schauen ist komplett kostenlos \\u2014 der VTuber Star Award ist ein Community-Event von Fans f\\u00FCr Fans.\"},{\"question\":\"Wann und wo findet die Award-Show statt?\",\"answer\":\"Die gro\\u00DFe Live-Show wird von Jayuhime gehostet und auf Twitch \\u0026 YouTube gestreamt. Den genauen Termin findest du im Countdown oben \\u2014 sei live dabei, wenn die Stars gek\\u00FCrt werden!\"},{\"question\":\"Ich wurde nominiert \\u2014 was nun?\",\"answer\":\"Gl\\u00FCckwunsch! Du erh\\u00E4ltst eine Benachrichtigung mit allen Infos. Teile deine Nominierung gern mit deiner Community und ruf zum Voten auf \\u2014 jede Stimme z\\u00E4hlt.\"}]", + HostDisplayName = "Jayuhime", + HostTagline = "VTuber & Award Host", + ImprintContent = "Anbieter\nVTuber Star Awards, vertreten durch Jayuhime.\n\nKontakt\nNutze fuer organisatorische Fragen bitte die Kontaktseite oder die hinterlegte Kontaktadresse.\n\nHinweis\nDieses Impressum ist ein redaktioneller Platzhalter und sollte vor dem Livegang mit den tatsaechlichen Anbieterangaben ersetzt werden.", + ImprintUrl = "https://vtuber-star-awards.de/impressum", + MaintenanceMessage = "Die Award-Galaxie wird gerade liebevoll poliert. Schau gleich wieder vorbei.", + MaintenanceModeEnabled = false, + MaintenanceTitle = "Sternenpause", + NewsletterUrl = "https://vtuber-star-awards.de/newsletter", + NominationLinkBlacklistJson = "[{\"Url\":\"https://kick.com/\"},{\"Url\":\"https://www.twitch.tv/\"},{\"Url\":\"https://www.youtube.com/\"}]", + PrivacyEmail = "datenschutz@vtuber-star-awards.de", + PrivacyPolicyContent = "Verantwortliche:r\nVTuber Star Awards, vertreten durch Jayuhime. Kontakt: datenschutz@vtuber-star-awards.de\n\nWelche Daten wir verarbeiten\nBei Teilnahme (Voting, Nominierung, Clip-Einreichung) verarbeiten wir ausschließlich deine Twitch-User-ID sowie den Zeitstempel deiner Aktion.\nFür Show-Erinnerungen speichern wir optional deine E-Mail-Adresse.\n\nRechtsgrundlage\nVerarbeitung auf Basis von Art. 6 Abs. 1 lit. b DSGVO. Für optionale Erinnerungen gilt Art. 6 Abs. 1 lit. a DSGVO.\n\nZweck der Verarbeitung\nDurchführung des VTuber Star Awards, Sicherstellung fairer Abstimmung, Spam-Prävention sowie Admin-Review von Einreichungen.\n\nLöschfristen\nAlle Teilnahmedaten werden spätestens 6 Monate nach der Award-Show automatisch gelöscht. E-Mail-Adressen werden nach der Show entfernt.\n\nDeine Rechte\nDu hast das Recht auf Auskunft, Löschung, Berichtigung, Widerspruch und Beschwerde bei der zuständigen Aufsichtsbehörde.\n\nWeitergabe an Dritte\nKeine Weitergabe zu Werbezwecken. Technische Dienstleister verarbeiten Daten ausschließlich im Rahmen der Auftragsverarbeitung.", + PrivacyPolicyUpdatedAt = new DateTimeOffset(new DateTime(2026, 6, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + PrivacyPolicyUpdatedBy = "seed", + RiskRulesJson = "[{\"key\":\"resubmitted_ballot\",\"label\":\"Ballot erneut gespeichert\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"low\",\"description\":\"Erstellt einen Hinweis, wenn ein User sein Voting erneut speichert.\"},{\"key\":\"rapid_vote_updates\",\"label\":\"Voting-Burst\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr schnell mehrere Voting-Aenderungen ausloest.\"},{\"key\":\"resubmitted_nomination\",\"label\":\"Nominierung erneut eingereicht\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"low\",\"description\":\"Erstellt einen Hinweis, wenn ein User in derselben Kategorie erneut nominiert.\"},{\"key\":\"rapid_nomination_burst\",\"label\":\"Nominierungs-Burst\",\"enabled\":true,\"threshold\":10,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr viele Nominierungen in kurzer Zeit sendet.\"},{\"key\":\"duplicate_clip_submission\",\"label\":\"Doppelter Clip\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn ein User denselben Clip erneut einreicht.\"},{\"key\":\"rapid_clip_burst\",\"label\":\"Clip-Burst\",\"enabled\":true,\"threshold\":5,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr viele Clips in kurzer Zeit sendet.\"},{\"key\":\"rapid_login_ip\",\"label\":\"Login-Burst pro IP\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":15,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn mehrere neue Sessions von derselben IP entstehen.\"},{\"key\":\"rapid_demo_login_ip\",\"label\":\"Demo-Login-Burst pro IP\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":15,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn mehrere Demo-Admin-Sessions von derselben IP entstehen.\"}]", + ShowactApplicationDisabledMessage = "Showact-Bewerbungen sind aktuell geschlossen.", + ShowactApplicationsEnabled = false, + ShowactsContent = "Informationen zu Showact-Bewerbungen, Ablauf und Kontakt fuer die Award-Show.", + ShowactsUrl = "https://vtuber-star-awards.de/showacts", + SocialLinksJson = "[{\"label\":\"Twitch\",\"platform\":\"twitch\",\"url\":\"https://twitch.tv/jayuhime\",\"icon\":\"twitch\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"YouTube\",\"platform\":\"youtube\",\"url\":\"https://youtube.com/c/Jayuhime\",\"icon\":\"youtube\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"X\",\"platform\":\"x\",\"url\":\"https://x.com/jayuhime\",\"icon\":\"x\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Instagram\",\"platform\":\"instagram\",\"url\":\"https://instagram.com/jayuhime\",\"icon\":\"instagram\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Discord\",\"platform\":\"discord\",\"url\":\"https://discord.gg/jayuhime\",\"icon\":\"discord\",\"showOnHost\":true,\"showOnCommunity\":true}]", + SponsorsContent = "Sponsoren & Partner\nHier koennen Sponsor:innen, Medienpartner, Community-Partner und Supporter des VTuber Star Awards vorgestellt werden.\n\nPartner werden im Rahmen der Show und auf den oeffentlichen Kontaktflaechen genannt, sobald sie final bestaetigt sind.", + SponsorsUrl = "https://vtuber-star-awards.de/partner", + SponsorsVisible = true, + TwitchAuthManagedByDatabase = false, + TwitchClientId = "", + TwitchClientSecret = "", + TwitchRedirectUri = "", + TwitchScope = "", + WorkflowRulesJson = "[{\"key\":\"max_finalists_per_category\",\"label\":\"Finale Kandidat:innen pro Kategorie\",\"enabled\":true,\"limit\":4,\"mode\":\"block\",\"description\":\"Begrenzt, wie viele finale Kandidat:innen in einer Kategorie vorbereitet werden.\"},{\"key\":\"max_candidate_appearances\",\"label\":\"Kandidaturen pro Person\",\"enabled\":true,\"limit\":2,\"mode\":\"warn\",\"description\":\"Warnt oder blockiert, wenn dieselbe Person in zu vielen Kategorien final auftaucht.\"},{\"key\":\"max_winner_placements\",\"label\":\"Gewinnerpl\\u00E4tze pro Person\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn dieselbe Person mehrfach als Gewinner:in gesetzt wird.\"},{\"key\":\"winner_requires_clip\",\"label\":\"Gewinner braucht Clip-Link\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn ein Gewinner ohne gepflegten YouTube-/Twitch-Clip gesetzt wird.\"}]" + }); + }); + + modelBuilder.Entity("Backend.Domain.Sponsor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsVisible") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tier") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WebsiteUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "IsVisible", "SortOrder"); + + b.ToTable("Sponsors"); + }); + + modelBuilder.Entity("Backend.Domain.TeamMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoundTwitchDisplayName") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("BoundTwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Login") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MustChangePassword") + .HasColumnType("boolean"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("PasswordResetAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("TwitchBoundAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("BoundTwitchUserId") + .IsUnique(); + + b.HasIndex("Login") + .IsUnique(); + + b.ToTable("TeamMembers"); + }); + + modelBuilder.Entity("Backend.Domain.TeamRolePermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PermissionsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("Role") + .IsUnique(); + + b.ToTable("TeamRolePermissions"); + }); + + modelBuilder.Entity("Backend.Domain.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SessionToken") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SessionToken") + .IsUnique(); + + b.ToTable("UserSessions"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "SubmittedByTwitchId") + .IsUnique(); + + b.ToTable("VoteBallots"); + + b.HasData( + new + { + Id = 1, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_1" + }, + new + { + Id = 2, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 5, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_2" + }); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BallotId") + .HasColumnType("integer"); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BallotId"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.ToTable("VoteEntries"); + + b.HasData( + new + { + Id = 1, + BallotId = 1, + CandidateId = 1, + CategoryId = 1 + }, + new + { + Id = 2, + BallotId = 1, + CandidateId = 4, + CategoryId = 2 + }, + new + { + Id = 3, + BallotId = 2, + CandidateId = 2, + CategoryId = 1 + }, + new + { + Id = 4, + BallotId = 2, + CandidateId = 6, + CategoryId = 3 + }); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Results") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.HasOne("Backend.Domain.Category", "Category") + .WithMany("Candidates") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Categories") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Candidate"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId"); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.ShowactApplication", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Sponsor", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.HasOne("Backend.Domain.VoteBallot", "Ballot") + .WithMany("Entries") + .HasForeignKey("BallotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ballot"); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Navigation("Candidates"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Navigation("Categories"); + + b.Navigation("Results"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Navigation("Entries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/Migrations/20260627163650_AddClipAdminMenuVisible.cs b/Backend/Migrations/20260627163650_AddClipAdminMenuVisible.cs new file mode 100644 index 0000000..892b213 --- /dev/null +++ b/Backend/Migrations/20260627163650_AddClipAdminMenuVisible.cs @@ -0,0 +1,152 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backend.Migrations +{ + /// + public partial class AddClipAdminMenuVisible : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(""" + ALTER TABLE "SiteSettings" + ADD COLUMN IF NOT EXISTS "ClipAdminMenuVisible" boolean NOT NULL DEFAULT FALSE; + + ALTER TABLE "SiteSettings" + ADD COLUMN IF NOT EXISTS "ShowactApplicationDisabledMessage" character varying(240) NOT NULL DEFAULT ''; + + ALTER TABLE "SiteSettings" + ADD COLUMN IF NOT EXISTS "ShowactApplicationsEnabled" boolean NOT NULL DEFAULT FALSE; + + ALTER TABLE "SiteSettings" + ADD COLUMN IF NOT EXISTS "ShowactsContent" text NOT NULL DEFAULT ''; + + ALTER TABLE "SiteSettings" + ADD COLUMN IF NOT EXISTS "ShowactsUrl" character varying(400) NOT NULL DEFAULT ''; + + ALTER TABLE "SiteSettings" + ADD COLUMN IF NOT EXISTS "SponsorsVisible" boolean NOT NULL DEFAULT TRUE; + """); + + migrationBuilder.Sql(""" + CREATE TABLE IF NOT EXISTS "ShowactApplications" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "SeasonId" integer NOT NULL, + "ArtistName" character varying(120) NOT NULL, + "ContactEmail" character varying(180) NOT NULL, + "ContactDiscord" character varying(120) NOT NULL, + "PlatformUrl" character varying(500) NOT NULL, + "PerformanceType" character varying(80) NOT NULL, + "Description" character varying(1000) NOT NULL, + "TechnicalNotes" character varying(1000) NOT NULL, + "ReferenceUrl" character varying(500) NOT NULL, + "Status" character varying(20) NOT NULL, + "ReviewNote" character varying(500) NULL, + "ReviewedByTwitchId" character varying(120) NULL, + "CreatedFromIp" character varying(80) NOT NULL, + "UserAgent" character varying(400) NOT NULL, + "CreatedAt" timestamp with time zone NOT NULL, + "ReviewedAt" timestamp with time zone NULL + ); + + CREATE TABLE IF NOT EXISTS "Sponsors" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "SeasonId" integer NOT NULL, + "Name" character varying(120) NOT NULL, + "WebsiteUrl" character varying(500) NOT NULL, + "LogoUrl" character varying(500) NOT NULL, + "Description" character varying(500) NOT NULL, + "Tier" character varying(80) NOT NULL, + "SortOrder" integer NOT NULL, + "IsVisible" boolean NOT NULL, + "CreatedAt" timestamp with time zone NOT NULL, + "UpdatedAt" timestamp with time zone NULL + ); + + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'FK_ShowactApplications_Seasons_SeasonId' + ) THEN + ALTER TABLE "ShowactApplications" + ADD CONSTRAINT "FK_ShowactApplications_Seasons_SeasonId" + FOREIGN KEY ("SeasonId") REFERENCES "Seasons" ("Id") + ON DELETE CASCADE; + END IF; + END $$; + + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'FK_Sponsors_Seasons_SeasonId' + ) THEN + ALTER TABLE "Sponsors" + ADD CONSTRAINT "FK_Sponsors_Seasons_SeasonId" + FOREIGN KEY ("SeasonId") REFERENCES "Seasons" ("Id") + ON DELETE CASCADE; + END IF; + END $$; + """); + + migrationBuilder.UpdateData( + table: "SiteSettings", + keyColumn: "Id", + keyValue: 1, + columns: new[] { "ClipAdminMenuVisible", "ShowactApplicationDisabledMessage", "ShowactsContent", "ShowactsUrl", "SponsorsVisible", "WorkflowRulesJson" }, + values: new object[] { true, "Showact-Bewerbungen sind aktuell geschlossen.", "Informationen zu Showact-Bewerbungen, Ablauf und Kontakt fuer die Award-Show.", "https://vtuber-star-awards.de/showacts", true, "[{\"key\":\"max_finalists_per_category\",\"label\":\"Finale Kandidat:innen pro Kategorie\",\"enabled\":true,\"limit\":4,\"mode\":\"block\",\"description\":\"Begrenzt, wie viele finale Kandidat:innen in einer Kategorie vorbereitet werden.\"},{\"key\":\"max_candidate_appearances\",\"label\":\"Kandidaturen pro Person\",\"enabled\":true,\"limit\":2,\"mode\":\"warn\",\"description\":\"Warnt oder blockiert, wenn dieselbe Person in zu vielen Kategorien final auftaucht.\"},{\"key\":\"max_winner_placements\",\"label\":\"Gewinnerpl\\u00E4tze pro Person\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn dieselbe Person mehrfach als Gewinner:in gesetzt wird.\"},{\"key\":\"winner_requires_clip\",\"label\":\"Gewinner braucht Clip-Link\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn ein Gewinner ohne gepflegten YouTube-/Twitch-Clip gesetzt wird.\"}]" }); + + migrationBuilder.Sql(""" + CREATE INDEX IF NOT EXISTS "IX_ShowactApplications_SeasonId_Status" + ON "ShowactApplications" ("SeasonId", "Status"); + + CREATE INDEX IF NOT EXISTS "IX_Sponsors_SeasonId_IsVisible_SortOrder" + ON "Sponsors" ("SeasonId", "IsVisible", "SortOrder"); + """); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(""" + DROP TABLE IF EXISTS "ShowactApplications"; + + DROP TABLE IF EXISTS "Sponsors"; + """); + + migrationBuilder.Sql(""" + ALTER TABLE "SiteSettings" + DROP COLUMN IF EXISTS "ClipAdminMenuVisible"; + + ALTER TABLE "SiteSettings" + DROP COLUMN IF EXISTS "ShowactApplicationDisabledMessage"; + + ALTER TABLE "SiteSettings" + DROP COLUMN IF EXISTS "ShowactApplicationsEnabled"; + + ALTER TABLE "SiteSettings" + DROP COLUMN IF EXISTS "ShowactsContent"; + + ALTER TABLE "SiteSettings" + DROP COLUMN IF EXISTS "ShowactsUrl"; + + ALTER TABLE "SiteSettings" + DROP COLUMN IF EXISTS "SponsorsVisible"; + """); + + migrationBuilder.UpdateData( + table: "SiteSettings", + keyColumn: "Id", + keyValue: 1, + column: "WorkflowRulesJson", + value: "[{\"key\":\"max_finalists_per_category\",\"label\":\"Finale Kandidat:innen pro Kategorie\",\"enabled\":true,\"limit\":4,\"mode\":\"block\",\"description\":\"Begrenzt, wie viele finale Kandidat:innen in einer Kategorie vorbereitet werden.\"},{\"key\":\"max_candidate_appearances\",\"label\":\"Kandidaturen pro Person\",\"enabled\":true,\"limit\":2,\"mode\":\"warn\",\"description\":\"Warnt oder blockiert, wenn dieselbe Person in zu vielen Kategorien final auftaucht.\"},{\"key\":\"max_winner_placements\",\"label\":\"Gewinnerpl\\u00E4tze pro Person\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn dieselbe Person mehrfach als Gewinner:in gesetzt wird.\"}]"); + } + } +} diff --git a/Backend/Migrations/20260627171320_AddShareUrls.Designer.cs b/Backend/Migrations/20260627171320_AddShareUrls.Designer.cs new file mode 100644 index 0000000..59237c0 --- /dev/null +++ b/Backend/Migrations/20260627171320_AddShareUrls.Designer.cs @@ -0,0 +1,1768 @@ +// +using System; +using Backend.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backend.Migrations +{ + [DbContext(typeof(AwardsDbContext))] + [Migration("20260627171320_AddShareUrls")] + partial class AddShareUrls + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backend.Domain.AdminAuditEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("AdminTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.ToTable("AdminAuditEntries"); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CategoryName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "CategoryId") + .IsUnique(); + + b.ToTable("Results"); + + b.HasData( + new + { + Id = 1, + CandidateId = 8, + CategoryId = 5, + CategoryName = "VTuber des Jahres", + SeasonId = 2 + }, + new + { + Id = 2, + CandidateId = 9, + CategoryId = 6, + CategoryName = "Bestes Live Event", + SeasonId = 2 + }, + new + { + Id = 3, + CandidateId = 10, + CategoryId = 7, + CategoryName = "Clip des Jahres", + SeasonId = 2 + }, + new + { + Id = 4, + CandidateId = 11, + CategoryId = 8, + CategoryName = "VTuber des Jahres", + SeasonId = 3 + }, + new + { + Id = 5, + CandidateId = 12, + CategoryId = 9, + CategoryName = "Clip des Jahres", + SeasonId = 3 + }, + new + { + Id = 6, + CandidateId = 13, + CategoryId = 10, + CategoryName = "VTuber des Jahres", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AcceptanceNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AcceptanceStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasDefaultValue("open"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ChannelSlug") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ClipCompilationPlatform") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ClipCompilationTitle") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ClipCompilationUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ClipEmbedStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasDefaultValue("unchecked"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId"); + + b.ToTable("Candidates"); + + b.HasData( + new + { + Id = 1, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@hoshimimiyu", + ClipEmbedStatus = "unchecked", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 2, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 3, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@shiroch", + ClipEmbedStatus = "unchecked", + DisplayName = "Shiro Ch.", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 4, + AcceptanceStatus = "open", + CategoryId = 2, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 5, + AcceptanceStatus = "open", + CategoryId = 2, + ChannelSlug = "@aoisakura", + ClipEmbedStatus = "unchecked", + DisplayName = "Aoi Sakura Showcase", + Platform = "YouTube", + SeasonId = 1 + }, + new + { + Id = 6, + AcceptanceStatus = "open", + CategoryId = 3, + ChannelSlug = "@pyonkichikingdom", + ClipEmbedStatus = "unchecked", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 7, + AcceptanceStatus = "open", + CategoryId = 4, + ChannelSlug = "@moonrelay", + ClipEmbedStatus = "unchecked", + DisplayName = "Moonrelay", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 8, + AcceptanceStatus = "open", + CategoryId = 5, + ChannelSlug = "@hoshimimiyu", + ClipEmbedStatus = "unchecked", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 9, + AcceptanceStatus = "open", + CategoryId = 6, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 10, + AcceptanceStatus = "open", + CategoryId = 7, + ChannelSlug = "@pyonkichikingdom", + ClipEmbedStatus = "unchecked", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 11, + AcceptanceStatus = "open", + CategoryId = 8, + ChannelSlug = "@aoisakura", + ClipEmbedStatus = "unchecked", + DisplayName = "Aoi Sakura", + Platform = "YouTube", + SeasonId = 3 + }, + new + { + Id = 12, + AcceptanceStatus = "open", + CategoryId = 9, + ChannelSlug = "@starbyte", + ClipEmbedStatus = "unchecked", + DisplayName = "Starbyte", + Platform = "Twitch", + SeasonId = 3 + }, + new + { + Id = 13, + AcceptanceStatus = "open", + CategoryId = 10, + ChannelSlug = "@tenshivox", + ClipEmbedStatus = "unchecked", + DisplayName = "Tenshi Vox", + Platform = "Twitch", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MaxNomineesPerUser") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Slug") + .IsUnique(); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = 1, + Description = "Die groesste Auszeichnung des Jahres.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 1, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 2, + Description = "Events, Konzerte und 3D-Shows.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 1, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 3, + Description = "Der lustigste oder emotionalste Clip des Jahres.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 1, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 4, + Description = "Die aktivste und freundlichste Community.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "Beste Community", + SeasonId = 1, + Slug = "beste-community", + SortOrder = 4 + }, + new + { + Id = 5, + Description = "Archivkategorie 2025.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 2, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 6, + Description = "Archivkategorie 2025.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 2, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 7, + Description = "Archivkategorie 2025.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 2, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 8, + Description = "Archivkategorie 2024.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 3, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 9, + Description = "Archivkategorie 2024.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 3, + Slug = "clip-des-jahres", + SortOrder = 2 + }, + new + { + Id = 10, + Description = "Archivkategorie 2023.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 4, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ClipUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Creator") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("ClipSubmissions"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CandidateText") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("StreamUrl") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("Nominations"); + + b.HasData( + new + { + Id = 1, + CandidateText = "Hoshimi Miyu", + CategoryId = 1, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 13, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_hoshi" + }, + new + { + Id = 2, + CandidateText = "Kurainu 3D Live", + CategoryId = 2, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 14, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_kurainu" + }); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("TwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("RiskFlags"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrentPhase") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("IsCommunityOnly") + .HasColumnType("boolean"); + + b.Property("IsCurrent") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("NominationEndsAt") + .HasColumnType("date"); + + b.Property("NominationStartsAt") + .HasColumnType("date"); + + b.Property("ReviewEndsAt") + .HasColumnType("date"); + + b.Property("ReviewStartsAt") + .HasColumnType("date"); + + b.Property("ShowDate") + .HasColumnType("date"); + + b.Property("ShowStartsAt") + .HasColumnType("time without time zone"); + + b.Property("ShowStreamUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("VotingEndsAt") + .HasColumnType("date"); + + b.Property("VotingStartsAt") + .HasColumnType("date"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("Seasons"); + + b.HasData( + new + { + Id = 1, + CurrentPhase = "Community Voting", + IsCommunityOnly = true, + IsCurrent = true, + Name = "VTuber Star Awards 2026", + NominationEndsAt = new DateOnly(2026, 5, 31), + NominationStartsAt = new DateOnly(2026, 5, 1), + ReviewEndsAt = new DateOnly(2026, 7, 10), + ReviewStartsAt = new DateOnly(2026, 7, 1), + ShowDate = new DateOnly(2026, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2026, 6, 30), + VotingStartsAt = new DateOnly(2026, 6, 1), + Year = 2026 + }, + new + { + Id = 2, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2025", + NominationEndsAt = new DateOnly(2025, 5, 31), + NominationStartsAt = new DateOnly(2025, 5, 1), + ReviewEndsAt = new DateOnly(2025, 7, 10), + ReviewStartsAt = new DateOnly(2025, 7, 1), + ShowDate = new DateOnly(2025, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2025, 6, 30), + VotingStartsAt = new DateOnly(2025, 6, 1), + Year = 2025 + }, + new + { + Id = 3, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2024", + NominationEndsAt = new DateOnly(2024, 5, 31), + NominationStartsAt = new DateOnly(2024, 5, 1), + ReviewEndsAt = new DateOnly(2024, 7, 10), + ReviewStartsAt = new DateOnly(2024, 7, 1), + ShowDate = new DateOnly(2024, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://youtube.com/c/Jayuhime", + VotingEndsAt = new DateOnly(2024, 6, 30), + VotingStartsAt = new DateOnly(2024, 6, 1), + Year = 2024 + }, + new + { + Id = 4, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2023", + NominationEndsAt = new DateOnly(2023, 5, 31), + NominationStartsAt = new DateOnly(2023, 5, 1), + ReviewEndsAt = new DateOnly(2023, 7, 10), + ReviewStartsAt = new DateOnly(2023, 7, 1), + ShowDate = new DateOnly(2023, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2023, 6, 30), + VotingStartsAt = new DateOnly(2023, 6, 1), + Year = 2023 + }); + }); + + modelBuilder.Entity("Backend.Domain.ShowactApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ArtistName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ContactDiscord") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ContactEmail") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("PerformanceType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("PlatformUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReferenceUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TechnicalNotes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("ShowactApplications"); + }); + + modelBuilder.Entity("Backend.Domain.SiteSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClipAdminMenuVisible") + .HasColumnType("boolean"); + + b.Property("ClipReviewEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ClipSubmissionDisabledMessage") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("ClipSubmissionsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ContactContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("DemoLoginDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginEmail") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("DemoLoginEnabled") + .HasColumnType("boolean"); + + b.Property("DemoLoginManagedByDatabase") + .HasColumnType("boolean"); + + b.Property("DemoLoginPasswordHash") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginPasswordSalt") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DemoLoginTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("FaqJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("HostDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("HostTagline") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("ImprintContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("ImprintUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("MaintenanceMessage") + .IsRequired() + .HasMaxLength(600) + .HasColumnType("character varying(600)"); + + b.Property("MaintenanceModeEnabled") + .HasColumnType("boolean"); + + b.Property("MaintenanceTitle") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("NewsletterUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("NominationLinkBlacklistJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("[]"); + + b.Property("PrivacyEmail") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("PrivacyPolicyContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("PrivacyPolicyUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivacyPolicyUpdatedBy") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("RiskRulesJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShareDiscordUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShareXUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShowactApplicationDisabledMessage") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("ShowactApplicationsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ShowactsContent") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue(""); + + b.Property("ShowactsUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("SocialLinksJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("SponsorsVisible") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("TwitchAuthManagedByDatabase") + .HasColumnType("boolean"); + + b.Property("TwitchClientId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchClientSecret") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("TwitchRedirectUri") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("TwitchScope") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("WorkflowRulesJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("[]"); + + b.HasKey("Id"); + + b.ToTable("SiteSettings"); + + b.HasData( + new + { + Id = 1, + ClipAdminMenuVisible = true, + ClipReviewEnabled = true, + ClipSubmissionDisabledMessage = "Clip-Einreichungen sind aktuell geschlossen.", + ClipSubmissionsEnabled = false, + ContactContent = "Kontakt zum Award-Team\nDu hast Fragen zur Nominierung, zum Voting, zu Clips oder zur Show? Schreib dem Team ueber die hinterlegte Kontaktseite.\n\nDatenschutzfragen\nFuer Datenschutzanfragen nutze bitte die Datenschutz-E-Mail aus diesem Footer.\n\nCommunity & Kooperationen\nSocial Links und Partneranfragen werden vom Admin-Team gepflegt und koordiniert.", + ContactUrl = "https://vtuber-star-awards.de/kontakt", + DemoLoginDisplayName = "Jayuhime Admin", + DemoLoginEmail = "", + DemoLoginEnabled = false, + DemoLoginManagedByDatabase = false, + DemoLoginPasswordHash = "", + DemoLoginPasswordSalt = "", + DemoLoginTwitchUserId = "jayuhime_admin", + FaqJson = "[{\"question\":\"Wer darf nominiert werden?\",\"answer\":\"Jede:r aktive deutschsprachige VTuber kann nominiert werden \\u2014 unabh\\u00E4ngig von Follower-Zahl oder Plattform. Die Community schl\\u00E4gt in der Nominierungsphase ihre Favorit:innen vor.\"},{\"question\":\"Wie funktioniert das Voting?\",\"answer\":\"Du meldest dich ausschlie\\u00DFlich mit deinem Twitch-Account an \\u2014 nur so kannst du teilnehmen. Das h\\u00E4lt das Voting fair und bot-frei. Danach vergibst du pro Kategorie eine Stimme, \\u00E4nderbar bis zum Ende der Phase.\"},{\"question\":\"Was kostet die Teilnahme?\",\"answer\":\"Gar nichts. Nominieren, voten und die Show schauen ist komplett kostenlos \\u2014 der VTuber Star Award ist ein Community-Event von Fans f\\u00FCr Fans.\"},{\"question\":\"Wann und wo findet die Award-Show statt?\",\"answer\":\"Die gro\\u00DFe Live-Show wird von Jayuhime gehostet und auf Twitch \\u0026 YouTube gestreamt. Den genauen Termin findest du im Countdown oben \\u2014 sei live dabei, wenn die Stars gek\\u00FCrt werden!\"},{\"question\":\"Ich wurde nominiert \\u2014 was nun?\",\"answer\":\"Gl\\u00FCckwunsch! Du erh\\u00E4ltst eine Benachrichtigung mit allen Infos. Teile deine Nominierung gern mit deiner Community und ruf zum Voten auf \\u2014 jede Stimme z\\u00E4hlt.\"}]", + HostDisplayName = "Jayuhime", + HostTagline = "VTuber & Award Host", + ImprintContent = "Anbieter\nVTuber Star Awards, vertreten durch Jayuhime.\n\nKontakt\nNutze fuer organisatorische Fragen bitte die Kontaktseite oder die hinterlegte Kontaktadresse.\n\nHinweis\nDieses Impressum ist ein redaktioneller Platzhalter und sollte vor dem Livegang mit den tatsaechlichen Anbieterangaben ersetzt werden.", + ImprintUrl = "https://vtuber-star-awards.de/impressum", + MaintenanceMessage = "Die Award-Galaxie wird gerade liebevoll poliert. Schau gleich wieder vorbei.", + MaintenanceModeEnabled = false, + MaintenanceTitle = "Sternenpause", + NewsletterUrl = "https://vtuber-star-awards.de/newsletter", + NominationLinkBlacklistJson = "[{\"Url\":\"https://kick.com/\"},{\"Url\":\"https://www.twitch.tv/\"},{\"Url\":\"https://www.youtube.com/\"}]", + PrivacyEmail = "datenschutz@vtuber-star-awards.de", + PrivacyPolicyContent = "Verantwortliche:r\nVTuber Star Awards, vertreten durch Jayuhime. Kontakt: datenschutz@vtuber-star-awards.de\n\nWelche Daten wir verarbeiten\nBei Teilnahme (Voting, Nominierung, Clip-Einreichung) verarbeiten wir ausschließlich deine Twitch-User-ID sowie den Zeitstempel deiner Aktion.\nFür Show-Erinnerungen speichern wir optional deine E-Mail-Adresse.\n\nRechtsgrundlage\nVerarbeitung auf Basis von Art. 6 Abs. 1 lit. b DSGVO. Für optionale Erinnerungen gilt Art. 6 Abs. 1 lit. a DSGVO.\n\nZweck der Verarbeitung\nDurchführung des VTuber Star Awards, Sicherstellung fairer Abstimmung, Spam-Prävention sowie Admin-Review von Einreichungen.\n\nLöschfristen\nAlle Teilnahmedaten werden spätestens 6 Monate nach der Award-Show automatisch gelöscht. E-Mail-Adressen werden nach der Show entfernt.\n\nDeine Rechte\nDu hast das Recht auf Auskunft, Löschung, Berichtigung, Widerspruch und Beschwerde bei der zuständigen Aufsichtsbehörde.\n\nWeitergabe an Dritte\nKeine Weitergabe zu Werbezwecken. Technische Dienstleister verarbeiten Daten ausschließlich im Rahmen der Auftragsverarbeitung.", + PrivacyPolicyUpdatedAt = new DateTimeOffset(new DateTime(2026, 6, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + PrivacyPolicyUpdatedBy = "seed", + RiskRulesJson = "[{\"key\":\"resubmitted_ballot\",\"label\":\"Ballot erneut gespeichert\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"low\",\"description\":\"Erstellt einen Hinweis, wenn ein User sein Voting erneut speichert.\"},{\"key\":\"rapid_vote_updates\",\"label\":\"Voting-Burst\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr schnell mehrere Voting-Aenderungen ausloest.\"},{\"key\":\"resubmitted_nomination\",\"label\":\"Nominierung erneut eingereicht\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"low\",\"description\":\"Erstellt einen Hinweis, wenn ein User in derselben Kategorie erneut nominiert.\"},{\"key\":\"rapid_nomination_burst\",\"label\":\"Nominierungs-Burst\",\"enabled\":true,\"threshold\":10,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr viele Nominierungen in kurzer Zeit sendet.\"},{\"key\":\"duplicate_clip_submission\",\"label\":\"Doppelter Clip\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn ein User denselben Clip erneut einreicht.\"},{\"key\":\"rapid_clip_burst\",\"label\":\"Clip-Burst\",\"enabled\":true,\"threshold\":5,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr viele Clips in kurzer Zeit sendet.\"},{\"key\":\"rapid_login_ip\",\"label\":\"Login-Burst pro IP\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":15,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn mehrere neue Sessions von derselben IP entstehen.\"},{\"key\":\"rapid_demo_login_ip\",\"label\":\"Demo-Login-Burst pro IP\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":15,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn mehrere Demo-Admin-Sessions von derselben IP entstehen.\"}]", + ShareDiscordUrl = "", + ShareXUrl = "", + ShowactApplicationDisabledMessage = "Showact-Bewerbungen sind aktuell geschlossen.", + ShowactApplicationsEnabled = false, + ShowactsContent = "Informationen zu Showact-Bewerbungen, Ablauf und Kontakt fuer die Award-Show.", + ShowactsUrl = "https://vtuber-star-awards.de/showacts", + SocialLinksJson = "[{\"label\":\"Twitch\",\"platform\":\"twitch\",\"url\":\"https://twitch.tv/jayuhime\",\"icon\":\"twitch\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"YouTube\",\"platform\":\"youtube\",\"url\":\"https://youtube.com/c/Jayuhime\",\"icon\":\"youtube\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"X\",\"platform\":\"x\",\"url\":\"https://x.com/jayuhime\",\"icon\":\"x\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Instagram\",\"platform\":\"instagram\",\"url\":\"https://instagram.com/jayuhime\",\"icon\":\"instagram\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Discord\",\"platform\":\"discord\",\"url\":\"https://discord.gg/jayuhime\",\"icon\":\"discord\",\"showOnHost\":true,\"showOnCommunity\":true}]", + SponsorsContent = "Sponsoren & Partner\nHier koennen Sponsor:innen, Medienpartner, Community-Partner und Supporter des VTuber Star Awards vorgestellt werden.\n\nPartner werden im Rahmen der Show und auf den oeffentlichen Kontaktflaechen genannt, sobald sie final bestaetigt sind.", + SponsorsUrl = "https://vtuber-star-awards.de/partner", + SponsorsVisible = true, + TwitchAuthManagedByDatabase = false, + TwitchClientId = "", + TwitchClientSecret = "", + TwitchRedirectUri = "", + TwitchScope = "", + WorkflowRulesJson = "[{\"key\":\"max_finalists_per_category\",\"label\":\"Finale Kandidat:innen pro Kategorie\",\"enabled\":true,\"limit\":4,\"mode\":\"block\",\"description\":\"Begrenzt, wie viele finale Kandidat:innen in einer Kategorie vorbereitet werden.\"},{\"key\":\"max_candidate_appearances\",\"label\":\"Kandidaturen pro Person\",\"enabled\":true,\"limit\":2,\"mode\":\"warn\",\"description\":\"Warnt oder blockiert, wenn dieselbe Person in zu vielen Kategorien final auftaucht.\"},{\"key\":\"max_winner_placements\",\"label\":\"Gewinnerpl\\u00E4tze pro Person\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn dieselbe Person mehrfach als Gewinner:in gesetzt wird.\"},{\"key\":\"winner_requires_clip\",\"label\":\"Gewinner braucht Clip-Link\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn ein Gewinner ohne gepflegten YouTube-/Twitch-Clip gesetzt wird.\"}]" + }); + }); + + modelBuilder.Entity("Backend.Domain.Sponsor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsVisible") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tier") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WebsiteUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "IsVisible", "SortOrder"); + + b.ToTable("Sponsors"); + }); + + modelBuilder.Entity("Backend.Domain.TeamMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoundTwitchDisplayName") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("BoundTwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Login") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MustChangePassword") + .HasColumnType("boolean"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("PasswordResetAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("TwitchBoundAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("BoundTwitchUserId") + .IsUnique(); + + b.HasIndex("Login") + .IsUnique(); + + b.ToTable("TeamMembers"); + }); + + modelBuilder.Entity("Backend.Domain.TeamRolePermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PermissionsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("Role") + .IsUnique(); + + b.ToTable("TeamRolePermissions"); + }); + + modelBuilder.Entity("Backend.Domain.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SessionToken") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SessionToken") + .IsUnique(); + + b.ToTable("UserSessions"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "SubmittedByTwitchId") + .IsUnique(); + + b.ToTable("VoteBallots"); + + b.HasData( + new + { + Id = 1, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_1" + }, + new + { + Id = 2, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 5, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_2" + }); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BallotId") + .HasColumnType("integer"); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BallotId"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.ToTable("VoteEntries"); + + b.HasData( + new + { + Id = 1, + BallotId = 1, + CandidateId = 1, + CategoryId = 1 + }, + new + { + Id = 2, + BallotId = 1, + CandidateId = 4, + CategoryId = 2 + }, + new + { + Id = 3, + BallotId = 2, + CandidateId = 2, + CategoryId = 1 + }, + new + { + Id = 4, + BallotId = 2, + CandidateId = 6, + CategoryId = 3 + }); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Results") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.HasOne("Backend.Domain.Category", "Category") + .WithMany("Candidates") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Categories") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Candidate"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId"); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.ShowactApplication", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Sponsor", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.HasOne("Backend.Domain.VoteBallot", "Ballot") + .WithMany("Entries") + .HasForeignKey("BallotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ballot"); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Navigation("Candidates"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Navigation("Categories"); + + b.Navigation("Results"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Navigation("Entries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/Migrations/20260627171320_AddShareUrls.cs b/Backend/Migrations/20260627171320_AddShareUrls.cs new file mode 100644 index 0000000..0402734 --- /dev/null +++ b/Backend/Migrations/20260627171320_AddShareUrls.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backend.Migrations +{ + /// + public partial class AddShareUrls : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ShareDiscordUrl", + table: "SiteSettings", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "ShareXUrl", + table: "SiteSettings", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.UpdateData( + table: "SiteSettings", + keyColumn: "Id", + keyValue: 1, + columns: new[] { "ShareDiscordUrl", "ShareXUrl" }, + values: new object[] { "", "" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ShareDiscordUrl", + table: "SiteSettings"); + + migrationBuilder.DropColumn( + name: "ShareXUrl", + table: "SiteSettings"); + } + } +} diff --git a/Backend/Migrations/20260627174243_AddShowactDynamicForm.Designer.cs b/Backend/Migrations/20260627174243_AddShowactDynamicForm.Designer.cs new file mode 100644 index 0000000..9c1cac8 --- /dev/null +++ b/Backend/Migrations/20260627174243_AddShowactDynamicForm.Designer.cs @@ -0,0 +1,1777 @@ +// +using System; +using Backend.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backend.Migrations +{ + [DbContext(typeof(AwardsDbContext))] + [Migration("20260627174243_AddShowactDynamicForm")] + partial class AddShowactDynamicForm + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backend.Domain.AdminAuditEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("AdminTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.ToTable("AdminAuditEntries"); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CategoryName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "CategoryId") + .IsUnique(); + + b.ToTable("Results"); + + b.HasData( + new + { + Id = 1, + CandidateId = 8, + CategoryId = 5, + CategoryName = "VTuber des Jahres", + SeasonId = 2 + }, + new + { + Id = 2, + CandidateId = 9, + CategoryId = 6, + CategoryName = "Bestes Live Event", + SeasonId = 2 + }, + new + { + Id = 3, + CandidateId = 10, + CategoryId = 7, + CategoryName = "Clip des Jahres", + SeasonId = 2 + }, + new + { + Id = 4, + CandidateId = 11, + CategoryId = 8, + CategoryName = "VTuber des Jahres", + SeasonId = 3 + }, + new + { + Id = 5, + CandidateId = 12, + CategoryId = 9, + CategoryName = "Clip des Jahres", + SeasonId = 3 + }, + new + { + Id = 6, + CandidateId = 13, + CategoryId = 10, + CategoryName = "VTuber des Jahres", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AcceptanceNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AcceptanceStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasDefaultValue("open"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ChannelSlug") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ClipCompilationPlatform") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ClipCompilationTitle") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ClipCompilationUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ClipEmbedStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasDefaultValue("unchecked"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId"); + + b.ToTable("Candidates"); + + b.HasData( + new + { + Id = 1, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@hoshimimiyu", + ClipEmbedStatus = "unchecked", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 2, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 3, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@shiroch", + ClipEmbedStatus = "unchecked", + DisplayName = "Shiro Ch.", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 4, + AcceptanceStatus = "open", + CategoryId = 2, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 5, + AcceptanceStatus = "open", + CategoryId = 2, + ChannelSlug = "@aoisakura", + ClipEmbedStatus = "unchecked", + DisplayName = "Aoi Sakura Showcase", + Platform = "YouTube", + SeasonId = 1 + }, + new + { + Id = 6, + AcceptanceStatus = "open", + CategoryId = 3, + ChannelSlug = "@pyonkichikingdom", + ClipEmbedStatus = "unchecked", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 7, + AcceptanceStatus = "open", + CategoryId = 4, + ChannelSlug = "@moonrelay", + ClipEmbedStatus = "unchecked", + DisplayName = "Moonrelay", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 8, + AcceptanceStatus = "open", + CategoryId = 5, + ChannelSlug = "@hoshimimiyu", + ClipEmbedStatus = "unchecked", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 9, + AcceptanceStatus = "open", + CategoryId = 6, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 10, + AcceptanceStatus = "open", + CategoryId = 7, + ChannelSlug = "@pyonkichikingdom", + ClipEmbedStatus = "unchecked", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 11, + AcceptanceStatus = "open", + CategoryId = 8, + ChannelSlug = "@aoisakura", + ClipEmbedStatus = "unchecked", + DisplayName = "Aoi Sakura", + Platform = "YouTube", + SeasonId = 3 + }, + new + { + Id = 12, + AcceptanceStatus = "open", + CategoryId = 9, + ChannelSlug = "@starbyte", + ClipEmbedStatus = "unchecked", + DisplayName = "Starbyte", + Platform = "Twitch", + SeasonId = 3 + }, + new + { + Id = 13, + AcceptanceStatus = "open", + CategoryId = 10, + ChannelSlug = "@tenshivox", + ClipEmbedStatus = "unchecked", + DisplayName = "Tenshi Vox", + Platform = "Twitch", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MaxNomineesPerUser") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Slug") + .IsUnique(); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = 1, + Description = "Die groesste Auszeichnung des Jahres.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 1, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 2, + Description = "Events, Konzerte und 3D-Shows.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 1, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 3, + Description = "Der lustigste oder emotionalste Clip des Jahres.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 1, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 4, + Description = "Die aktivste und freundlichste Community.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "Beste Community", + SeasonId = 1, + Slug = "beste-community", + SortOrder = 4 + }, + new + { + Id = 5, + Description = "Archivkategorie 2025.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 2, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 6, + Description = "Archivkategorie 2025.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 2, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 7, + Description = "Archivkategorie 2025.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 2, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 8, + Description = "Archivkategorie 2024.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 3, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 9, + Description = "Archivkategorie 2024.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 3, + Slug = "clip-des-jahres", + SortOrder = 2 + }, + new + { + Id = 10, + Description = "Archivkategorie 2023.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 4, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ClipUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Creator") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("ClipSubmissions"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CandidateText") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("StreamUrl") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("Nominations"); + + b.HasData( + new + { + Id = 1, + CandidateText = "Hoshimi Miyu", + CategoryId = 1, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 13, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_hoshi" + }, + new + { + Id = 2, + CandidateText = "Kurainu 3D Live", + CategoryId = 2, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 14, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_kurainu" + }); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("TwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("RiskFlags"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrentPhase") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("IsCommunityOnly") + .HasColumnType("boolean"); + + b.Property("IsCurrent") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("NominationEndsAt") + .HasColumnType("date"); + + b.Property("NominationStartsAt") + .HasColumnType("date"); + + b.Property("ReviewEndsAt") + .HasColumnType("date"); + + b.Property("ReviewStartsAt") + .HasColumnType("date"); + + b.Property("ShowDate") + .HasColumnType("date"); + + b.Property("ShowStartsAt") + .HasColumnType("time without time zone"); + + b.Property("ShowStreamUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("VotingEndsAt") + .HasColumnType("date"); + + b.Property("VotingStartsAt") + .HasColumnType("date"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("Seasons"); + + b.HasData( + new + { + Id = 1, + CurrentPhase = "Community Voting", + IsCommunityOnly = true, + IsCurrent = true, + Name = "VTuber Star Awards 2026", + NominationEndsAt = new DateOnly(2026, 5, 31), + NominationStartsAt = new DateOnly(2026, 5, 1), + ReviewEndsAt = new DateOnly(2026, 7, 10), + ReviewStartsAt = new DateOnly(2026, 7, 1), + ShowDate = new DateOnly(2026, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2026, 6, 30), + VotingStartsAt = new DateOnly(2026, 6, 1), + Year = 2026 + }, + new + { + Id = 2, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2025", + NominationEndsAt = new DateOnly(2025, 5, 31), + NominationStartsAt = new DateOnly(2025, 5, 1), + ReviewEndsAt = new DateOnly(2025, 7, 10), + ReviewStartsAt = new DateOnly(2025, 7, 1), + ShowDate = new DateOnly(2025, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2025, 6, 30), + VotingStartsAt = new DateOnly(2025, 6, 1), + Year = 2025 + }, + new + { + Id = 3, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2024", + NominationEndsAt = new DateOnly(2024, 5, 31), + NominationStartsAt = new DateOnly(2024, 5, 1), + ReviewEndsAt = new DateOnly(2024, 7, 10), + ReviewStartsAt = new DateOnly(2024, 7, 1), + ShowDate = new DateOnly(2024, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://youtube.com/c/Jayuhime", + VotingEndsAt = new DateOnly(2024, 6, 30), + VotingStartsAt = new DateOnly(2024, 6, 1), + Year = 2024 + }, + new + { + Id = 4, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2023", + NominationEndsAt = new DateOnly(2023, 5, 31), + NominationStartsAt = new DateOnly(2023, 5, 1), + ReviewEndsAt = new DateOnly(2023, 7, 10), + ReviewStartsAt = new DateOnly(2023, 7, 1), + ShowDate = new DateOnly(2023, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2023, 6, 30), + VotingStartsAt = new DateOnly(2023, 6, 1), + Year = 2023 + }); + }); + + modelBuilder.Entity("Backend.Domain.ShowactApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ArtistName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ContactDiscord") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ContactEmail") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("FieldResponsesJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("PerformanceType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("PlatformUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReferenceUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TechnicalNotes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("ShowactApplications"); + }); + + modelBuilder.Entity("Backend.Domain.SiteSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClipAdminMenuVisible") + .HasColumnType("boolean"); + + b.Property("ClipReviewEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ClipSubmissionDisabledMessage") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("ClipSubmissionsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ContactContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("DemoLoginDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginEmail") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("DemoLoginEnabled") + .HasColumnType("boolean"); + + b.Property("DemoLoginManagedByDatabase") + .HasColumnType("boolean"); + + b.Property("DemoLoginPasswordHash") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginPasswordSalt") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DemoLoginTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("FaqJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("HostDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("HostTagline") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("ImprintContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("ImprintUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("MaintenanceMessage") + .IsRequired() + .HasMaxLength(600) + .HasColumnType("character varying(600)"); + + b.Property("MaintenanceModeEnabled") + .HasColumnType("boolean"); + + b.Property("MaintenanceTitle") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("NewsletterUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("NominationLinkBlacklistJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("[]"); + + b.Property("PrivacyEmail") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("PrivacyPolicyContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("PrivacyPolicyUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivacyPolicyUpdatedBy") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("RiskRulesJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShareDiscordUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShareXUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShowactApplicationDisabledMessage") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("ShowactApplicationsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ShowactFormSchemaJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShowactsContent") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue(""); + + b.Property("ShowactsUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("SocialLinksJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("SponsorsVisible") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("TwitchAuthManagedByDatabase") + .HasColumnType("boolean"); + + b.Property("TwitchClientId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchClientSecret") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("TwitchRedirectUri") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("TwitchScope") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("WorkflowRulesJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("[]"); + + b.HasKey("Id"); + + b.ToTable("SiteSettings"); + + b.HasData( + new + { + Id = 1, + ClipAdminMenuVisible = true, + ClipReviewEnabled = true, + ClipSubmissionDisabledMessage = "Clip-Einreichungen sind aktuell geschlossen.", + ClipSubmissionsEnabled = false, + ContactContent = "Kontakt zum Award-Team\nDu hast Fragen zur Nominierung, zum Voting, zu Clips oder zur Show? Schreib dem Team ueber die hinterlegte Kontaktseite.\n\nDatenschutzfragen\nFuer Datenschutzanfragen nutze bitte die Datenschutz-E-Mail aus diesem Footer.\n\nCommunity & Kooperationen\nSocial Links und Partneranfragen werden vom Admin-Team gepflegt und koordiniert.", + ContactUrl = "https://vtuber-star-awards.de/kontakt", + DemoLoginDisplayName = "Jayuhime Admin", + DemoLoginEmail = "", + DemoLoginEnabled = false, + DemoLoginManagedByDatabase = false, + DemoLoginPasswordHash = "", + DemoLoginPasswordSalt = "", + DemoLoginTwitchUserId = "jayuhime_admin", + FaqJson = "[{\"question\":\"Wer darf nominiert werden?\",\"answer\":\"Jede:r aktive deutschsprachige VTuber kann nominiert werden \\u2014 unabh\\u00E4ngig von Follower-Zahl oder Plattform. Die Community schl\\u00E4gt in der Nominierungsphase ihre Favorit:innen vor.\"},{\"question\":\"Wie funktioniert das Voting?\",\"answer\":\"Du meldest dich ausschlie\\u00DFlich mit deinem Twitch-Account an \\u2014 nur so kannst du teilnehmen. Das h\\u00E4lt das Voting fair und bot-frei. Danach vergibst du pro Kategorie eine Stimme, \\u00E4nderbar bis zum Ende der Phase.\"},{\"question\":\"Was kostet die Teilnahme?\",\"answer\":\"Gar nichts. Nominieren, voten und die Show schauen ist komplett kostenlos \\u2014 der VTuber Star Award ist ein Community-Event von Fans f\\u00FCr Fans.\"},{\"question\":\"Wann und wo findet die Award-Show statt?\",\"answer\":\"Die gro\\u00DFe Live-Show wird von Jayuhime gehostet und auf Twitch \\u0026 YouTube gestreamt. Den genauen Termin findest du im Countdown oben \\u2014 sei live dabei, wenn die Stars gek\\u00FCrt werden!\"},{\"question\":\"Ich wurde nominiert \\u2014 was nun?\",\"answer\":\"Gl\\u00FCckwunsch! Du erh\\u00E4ltst eine Benachrichtigung mit allen Infos. Teile deine Nominierung gern mit deiner Community und ruf zum Voten auf \\u2014 jede Stimme z\\u00E4hlt.\"}]", + HostDisplayName = "Jayuhime", + HostTagline = "VTuber & Award Host", + ImprintContent = "Anbieter\nVTuber Star Awards, vertreten durch Jayuhime.\n\nKontakt\nNutze fuer organisatorische Fragen bitte die Kontaktseite oder die hinterlegte Kontaktadresse.\n\nHinweis\nDieses Impressum ist ein redaktioneller Platzhalter und sollte vor dem Livegang mit den tatsaechlichen Anbieterangaben ersetzt werden.", + ImprintUrl = "https://vtuber-star-awards.de/impressum", + MaintenanceMessage = "Die Award-Galaxie wird gerade liebevoll poliert. Schau gleich wieder vorbei.", + MaintenanceModeEnabled = false, + MaintenanceTitle = "Sternenpause", + NewsletterUrl = "https://vtuber-star-awards.de/newsletter", + NominationLinkBlacklistJson = "[{\"Url\":\"https://kick.com/\"},{\"Url\":\"https://www.twitch.tv/\"},{\"Url\":\"https://www.youtube.com/\"}]", + PrivacyEmail = "datenschutz@vtuber-star-awards.de", + PrivacyPolicyContent = "Verantwortliche:r\nVTuber Star Awards, vertreten durch Jayuhime. Kontakt: datenschutz@vtuber-star-awards.de\n\nWelche Daten wir verarbeiten\nBei Teilnahme (Voting, Nominierung, Clip-Einreichung) verarbeiten wir ausschließlich deine Twitch-User-ID sowie den Zeitstempel deiner Aktion.\nFür Show-Erinnerungen speichern wir optional deine E-Mail-Adresse.\n\nRechtsgrundlage\nVerarbeitung auf Basis von Art. 6 Abs. 1 lit. b DSGVO. Für optionale Erinnerungen gilt Art. 6 Abs. 1 lit. a DSGVO.\n\nZweck der Verarbeitung\nDurchführung des VTuber Star Awards, Sicherstellung fairer Abstimmung, Spam-Prävention sowie Admin-Review von Einreichungen.\n\nLöschfristen\nAlle Teilnahmedaten werden spätestens 6 Monate nach der Award-Show automatisch gelöscht. E-Mail-Adressen werden nach der Show entfernt.\n\nDeine Rechte\nDu hast das Recht auf Auskunft, Löschung, Berichtigung, Widerspruch und Beschwerde bei der zuständigen Aufsichtsbehörde.\n\nWeitergabe an Dritte\nKeine Weitergabe zu Werbezwecken. Technische Dienstleister verarbeiten Daten ausschließlich im Rahmen der Auftragsverarbeitung.", + PrivacyPolicyUpdatedAt = new DateTimeOffset(new DateTime(2026, 6, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + PrivacyPolicyUpdatedBy = "seed", + RiskRulesJson = "[{\"key\":\"resubmitted_ballot\",\"label\":\"Ballot erneut gespeichert\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"low\",\"description\":\"Erstellt einen Hinweis, wenn ein User sein Voting erneut speichert.\"},{\"key\":\"rapid_vote_updates\",\"label\":\"Voting-Burst\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr schnell mehrere Voting-Aenderungen ausloest.\"},{\"key\":\"resubmitted_nomination\",\"label\":\"Nominierung erneut eingereicht\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"low\",\"description\":\"Erstellt einen Hinweis, wenn ein User in derselben Kategorie erneut nominiert.\"},{\"key\":\"rapid_nomination_burst\",\"label\":\"Nominierungs-Burst\",\"enabled\":true,\"threshold\":10,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr viele Nominierungen in kurzer Zeit sendet.\"},{\"key\":\"duplicate_clip_submission\",\"label\":\"Doppelter Clip\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn ein User denselben Clip erneut einreicht.\"},{\"key\":\"rapid_clip_burst\",\"label\":\"Clip-Burst\",\"enabled\":true,\"threshold\":5,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr viele Clips in kurzer Zeit sendet.\"},{\"key\":\"rapid_login_ip\",\"label\":\"Login-Burst pro IP\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":15,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn mehrere neue Sessions von derselben IP entstehen.\"},{\"key\":\"rapid_demo_login_ip\",\"label\":\"Demo-Login-Burst pro IP\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":15,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn mehrere Demo-Admin-Sessions von derselben IP entstehen.\"}]", + ShareDiscordUrl = "", + ShareXUrl = "", + ShowactApplicationDisabledMessage = "Showact-Bewerbungen sind aktuell geschlossen.", + ShowactApplicationsEnabled = false, + ShowactFormSchemaJson = "[]", + ShowactsContent = "Informationen zu Showact-Bewerbungen, Ablauf und Kontakt fuer die Award-Show.", + ShowactsUrl = "https://vtuber-star-awards.de/showacts", + SocialLinksJson = "[{\"label\":\"Twitch\",\"platform\":\"twitch\",\"url\":\"https://twitch.tv/jayuhime\",\"icon\":\"twitch\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"YouTube\",\"platform\":\"youtube\",\"url\":\"https://youtube.com/c/Jayuhime\",\"icon\":\"youtube\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"X\",\"platform\":\"x\",\"url\":\"https://x.com/jayuhime\",\"icon\":\"x\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Instagram\",\"platform\":\"instagram\",\"url\":\"https://instagram.com/jayuhime\",\"icon\":\"instagram\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Discord\",\"platform\":\"discord\",\"url\":\"https://discord.gg/jayuhime\",\"icon\":\"discord\",\"showOnHost\":true,\"showOnCommunity\":true}]", + SponsorsContent = "Sponsoren & Partner\nHier koennen Sponsor:innen, Medienpartner, Community-Partner und Supporter des VTuber Star Awards vorgestellt werden.\n\nPartner werden im Rahmen der Show und auf den oeffentlichen Kontaktflaechen genannt, sobald sie final bestaetigt sind.", + SponsorsUrl = "https://vtuber-star-awards.de/partner", + SponsorsVisible = true, + TwitchAuthManagedByDatabase = false, + TwitchClientId = "", + TwitchClientSecret = "", + TwitchRedirectUri = "", + TwitchScope = "", + WorkflowRulesJson = "[{\"key\":\"max_finalists_per_category\",\"label\":\"Finale Kandidat:innen pro Kategorie\",\"enabled\":true,\"limit\":4,\"mode\":\"block\",\"description\":\"Begrenzt, wie viele finale Kandidat:innen in einer Kategorie vorbereitet werden.\"},{\"key\":\"max_candidate_appearances\",\"label\":\"Kandidaturen pro Person\",\"enabled\":true,\"limit\":2,\"mode\":\"warn\",\"description\":\"Warnt oder blockiert, wenn dieselbe Person in zu vielen Kategorien final auftaucht.\"},{\"key\":\"max_winner_placements\",\"label\":\"Gewinnerpl\\u00E4tze pro Person\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn dieselbe Person mehrfach als Gewinner:in gesetzt wird.\"},{\"key\":\"winner_requires_clip\",\"label\":\"Gewinner braucht Clip-Link\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn ein Gewinner ohne gepflegten YouTube-/Twitch-Clip gesetzt wird.\"}]" + }); + }); + + modelBuilder.Entity("Backend.Domain.Sponsor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsVisible") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tier") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WebsiteUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "IsVisible", "SortOrder"); + + b.ToTable("Sponsors"); + }); + + modelBuilder.Entity("Backend.Domain.TeamMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoundTwitchDisplayName") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("BoundTwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Login") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MustChangePassword") + .HasColumnType("boolean"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("PasswordResetAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("TwitchBoundAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("BoundTwitchUserId") + .IsUnique(); + + b.HasIndex("Login") + .IsUnique(); + + b.ToTable("TeamMembers"); + }); + + modelBuilder.Entity("Backend.Domain.TeamRolePermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PermissionsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("Role") + .IsUnique(); + + b.ToTable("TeamRolePermissions"); + }); + + modelBuilder.Entity("Backend.Domain.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SessionToken") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SessionToken") + .IsUnique(); + + b.ToTable("UserSessions"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "SubmittedByTwitchId") + .IsUnique(); + + b.ToTable("VoteBallots"); + + b.HasData( + new + { + Id = 1, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_1" + }, + new + { + Id = 2, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 5, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_2" + }); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BallotId") + .HasColumnType("integer"); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BallotId"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.ToTable("VoteEntries"); + + b.HasData( + new + { + Id = 1, + BallotId = 1, + CandidateId = 1, + CategoryId = 1 + }, + new + { + Id = 2, + BallotId = 1, + CandidateId = 4, + CategoryId = 2 + }, + new + { + Id = 3, + BallotId = 2, + CandidateId = 2, + CategoryId = 1 + }, + new + { + Id = 4, + BallotId = 2, + CandidateId = 6, + CategoryId = 3 + }); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Results") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.HasOne("Backend.Domain.Category", "Category") + .WithMany("Candidates") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Categories") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Candidate"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId"); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.ShowactApplication", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Sponsor", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.HasOne("Backend.Domain.VoteBallot", "Ballot") + .WithMany("Entries") + .HasForeignKey("BallotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ballot"); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Navigation("Candidates"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Navigation("Categories"); + + b.Navigation("Results"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Navigation("Entries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/Migrations/20260627174243_AddShowactDynamicForm.cs b/Backend/Migrations/20260627174243_AddShowactDynamicForm.cs new file mode 100644 index 0000000..554f778 --- /dev/null +++ b/Backend/Migrations/20260627174243_AddShowactDynamicForm.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backend.Migrations +{ + /// + public partial class AddShowactDynamicForm : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ShowactFormSchemaJson", + table: "SiteSettings", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "FieldResponsesJson", + table: "ShowactApplications", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.UpdateData( + table: "SiteSettings", + keyColumn: "Id", + keyValue: 1, + column: "ShowactFormSchemaJson", + value: "[]"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ShowactFormSchemaJson", + table: "SiteSettings"); + + migrationBuilder.DropColumn( + name: "FieldResponsesJson", + table: "ShowactApplications"); + } + } +} diff --git a/Backend/Migrations/20260628091734_AddCategoryViewerRanges.Designer.cs b/Backend/Migrations/20260628091734_AddCategoryViewerRanges.Designer.cs new file mode 100644 index 0000000..e91e905 --- /dev/null +++ b/Backend/Migrations/20260628091734_AddCategoryViewerRanges.Designer.cs @@ -0,0 +1,1783 @@ +// +using System; +using Backend.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backend.Migrations +{ + [DbContext(typeof(AwardsDbContext))] + [Migration("20260628091734_AddCategoryViewerRanges")] + partial class AddCategoryViewerRanges + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backend.Domain.AdminAuditEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("AdminTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.ToTable("AdminAuditEntries"); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CategoryName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "CategoryId") + .IsUnique(); + + b.ToTable("Results"); + + b.HasData( + new + { + Id = 1, + CandidateId = 8, + CategoryId = 5, + CategoryName = "VTuber des Jahres", + SeasonId = 2 + }, + new + { + Id = 2, + CandidateId = 9, + CategoryId = 6, + CategoryName = "Bestes Live Event", + SeasonId = 2 + }, + new + { + Id = 3, + CandidateId = 10, + CategoryId = 7, + CategoryName = "Clip des Jahres", + SeasonId = 2 + }, + new + { + Id = 4, + CandidateId = 11, + CategoryId = 8, + CategoryName = "VTuber des Jahres", + SeasonId = 3 + }, + new + { + Id = 5, + CandidateId = 12, + CategoryId = 9, + CategoryName = "Clip des Jahres", + SeasonId = 3 + }, + new + { + Id = 6, + CandidateId = 13, + CategoryId = 10, + CategoryName = "VTuber des Jahres", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AcceptanceNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AcceptanceStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasDefaultValue("open"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ChannelSlug") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ClipCompilationPlatform") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ClipCompilationTitle") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ClipCompilationUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ClipEmbedStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasDefaultValue("unchecked"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId"); + + b.ToTable("Candidates"); + + b.HasData( + new + { + Id = 1, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@hoshimimiyu", + ClipEmbedStatus = "unchecked", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 2, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 3, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@shiroch", + ClipEmbedStatus = "unchecked", + DisplayName = "Shiro Ch.", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 4, + AcceptanceStatus = "open", + CategoryId = 2, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 5, + AcceptanceStatus = "open", + CategoryId = 2, + ChannelSlug = "@aoisakura", + ClipEmbedStatus = "unchecked", + DisplayName = "Aoi Sakura Showcase", + Platform = "YouTube", + SeasonId = 1 + }, + new + { + Id = 6, + AcceptanceStatus = "open", + CategoryId = 3, + ChannelSlug = "@pyonkichikingdom", + ClipEmbedStatus = "unchecked", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 7, + AcceptanceStatus = "open", + CategoryId = 4, + ChannelSlug = "@moonrelay", + ClipEmbedStatus = "unchecked", + DisplayName = "Moonrelay", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 8, + AcceptanceStatus = "open", + CategoryId = 5, + ChannelSlug = "@hoshimimiyu", + ClipEmbedStatus = "unchecked", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 9, + AcceptanceStatus = "open", + CategoryId = 6, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 10, + AcceptanceStatus = "open", + CategoryId = 7, + ChannelSlug = "@pyonkichikingdom", + ClipEmbedStatus = "unchecked", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 11, + AcceptanceStatus = "open", + CategoryId = 8, + ChannelSlug = "@aoisakura", + ClipEmbedStatus = "unchecked", + DisplayName = "Aoi Sakura", + Platform = "YouTube", + SeasonId = 3 + }, + new + { + Id = 12, + AcceptanceStatus = "open", + CategoryId = 9, + ChannelSlug = "@starbyte", + ClipEmbedStatus = "unchecked", + DisplayName = "Starbyte", + Platform = "Twitch", + SeasonId = 3 + }, + new + { + Id = 13, + AcceptanceStatus = "open", + CategoryId = 10, + ChannelSlug = "@tenshivox", + ClipEmbedStatus = "unchecked", + DisplayName = "Tenshi Vox", + Platform = "Twitch", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MaxNomineesPerUser") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("ViewerRangeMax") + .HasColumnType("integer"); + + b.Property("ViewerRangeMin") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Slug") + .IsUnique(); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = 1, + Description = "Die groesste Auszeichnung des Jahres.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 1, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 2, + Description = "Events, Konzerte und 3D-Shows.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 1, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 3, + Description = "Der lustigste oder emotionalste Clip des Jahres.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 1, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 4, + Description = "Die aktivste und freundlichste Community.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "Beste Community", + SeasonId = 1, + Slug = "beste-community", + SortOrder = 4 + }, + new + { + Id = 5, + Description = "Archivkategorie 2025.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 2, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 6, + Description = "Archivkategorie 2025.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 2, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 7, + Description = "Archivkategorie 2025.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 2, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 8, + Description = "Archivkategorie 2024.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 3, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 9, + Description = "Archivkategorie 2024.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 3, + Slug = "clip-des-jahres", + SortOrder = 2 + }, + new + { + Id = 10, + Description = "Archivkategorie 2023.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 4, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ClipUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Creator") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("ClipSubmissions"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CandidateText") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("StreamUrl") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("Nominations"); + + b.HasData( + new + { + Id = 1, + CandidateText = "Hoshimi Miyu", + CategoryId = 1, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 13, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_hoshi" + }, + new + { + Id = 2, + CandidateText = "Kurainu 3D Live", + CategoryId = 2, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 14, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_kurainu" + }); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("TwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("RiskFlags"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrentPhase") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("IsCommunityOnly") + .HasColumnType("boolean"); + + b.Property("IsCurrent") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("NominationEndsAt") + .HasColumnType("date"); + + b.Property("NominationStartsAt") + .HasColumnType("date"); + + b.Property("ReviewEndsAt") + .HasColumnType("date"); + + b.Property("ReviewStartsAt") + .HasColumnType("date"); + + b.Property("ShowDate") + .HasColumnType("date"); + + b.Property("ShowStartsAt") + .HasColumnType("time without time zone"); + + b.Property("ShowStreamUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("VotingEndsAt") + .HasColumnType("date"); + + b.Property("VotingStartsAt") + .HasColumnType("date"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("Seasons"); + + b.HasData( + new + { + Id = 1, + CurrentPhase = "Community Voting", + IsCommunityOnly = true, + IsCurrent = true, + Name = "VTuber Star Awards 2026", + NominationEndsAt = new DateOnly(2026, 5, 31), + NominationStartsAt = new DateOnly(2026, 5, 1), + ReviewEndsAt = new DateOnly(2026, 7, 10), + ReviewStartsAt = new DateOnly(2026, 7, 1), + ShowDate = new DateOnly(2026, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2026, 6, 30), + VotingStartsAt = new DateOnly(2026, 6, 1), + Year = 2026 + }, + new + { + Id = 2, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2025", + NominationEndsAt = new DateOnly(2025, 5, 31), + NominationStartsAt = new DateOnly(2025, 5, 1), + ReviewEndsAt = new DateOnly(2025, 7, 10), + ReviewStartsAt = new DateOnly(2025, 7, 1), + ShowDate = new DateOnly(2025, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2025, 6, 30), + VotingStartsAt = new DateOnly(2025, 6, 1), + Year = 2025 + }, + new + { + Id = 3, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2024", + NominationEndsAt = new DateOnly(2024, 5, 31), + NominationStartsAt = new DateOnly(2024, 5, 1), + ReviewEndsAt = new DateOnly(2024, 7, 10), + ReviewStartsAt = new DateOnly(2024, 7, 1), + ShowDate = new DateOnly(2024, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://youtube.com/c/Jayuhime", + VotingEndsAt = new DateOnly(2024, 6, 30), + VotingStartsAt = new DateOnly(2024, 6, 1), + Year = 2024 + }, + new + { + Id = 4, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2023", + NominationEndsAt = new DateOnly(2023, 5, 31), + NominationStartsAt = new DateOnly(2023, 5, 1), + ReviewEndsAt = new DateOnly(2023, 7, 10), + ReviewStartsAt = new DateOnly(2023, 7, 1), + ShowDate = new DateOnly(2023, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2023, 6, 30), + VotingStartsAt = new DateOnly(2023, 6, 1), + Year = 2023 + }); + }); + + modelBuilder.Entity("Backend.Domain.ShowactApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ArtistName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ContactDiscord") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ContactEmail") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("FieldResponsesJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("PerformanceType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("PlatformUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReferenceUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TechnicalNotes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("ShowactApplications"); + }); + + modelBuilder.Entity("Backend.Domain.SiteSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClipAdminMenuVisible") + .HasColumnType("boolean"); + + b.Property("ClipReviewEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ClipSubmissionDisabledMessage") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("ClipSubmissionsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ContactContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("DemoLoginDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginEmail") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("DemoLoginEnabled") + .HasColumnType("boolean"); + + b.Property("DemoLoginManagedByDatabase") + .HasColumnType("boolean"); + + b.Property("DemoLoginPasswordHash") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginPasswordSalt") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DemoLoginTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("FaqJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("HostDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("HostTagline") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("ImprintContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("ImprintUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("MaintenanceMessage") + .IsRequired() + .HasMaxLength(600) + .HasColumnType("character varying(600)"); + + b.Property("MaintenanceModeEnabled") + .HasColumnType("boolean"); + + b.Property("MaintenanceTitle") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("NewsletterUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("NominationLinkBlacklistJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("[]"); + + b.Property("PrivacyEmail") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("PrivacyPolicyContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("PrivacyPolicyUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivacyPolicyUpdatedBy") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("RiskRulesJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShareDiscordUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShareXUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShowactApplicationDisabledMessage") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("ShowactApplicationsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ShowactFormSchemaJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShowactsContent") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue(""); + + b.Property("ShowactsUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("SocialLinksJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("SponsorsVisible") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("TwitchAuthManagedByDatabase") + .HasColumnType("boolean"); + + b.Property("TwitchClientId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchClientSecret") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("TwitchRedirectUri") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("TwitchScope") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("WorkflowRulesJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("[]"); + + b.HasKey("Id"); + + b.ToTable("SiteSettings"); + + b.HasData( + new + { + Id = 1, + ClipAdminMenuVisible = true, + ClipReviewEnabled = true, + ClipSubmissionDisabledMessage = "Clip-Einreichungen sind aktuell geschlossen.", + ClipSubmissionsEnabled = false, + ContactContent = "Kontakt zum Award-Team\nDu hast Fragen zur Nominierung, zum Voting, zu Clips oder zur Show? Schreib dem Team ueber die hinterlegte Kontaktseite.\n\nDatenschutzfragen\nFuer Datenschutzanfragen nutze bitte die Datenschutz-E-Mail aus diesem Footer.\n\nCommunity & Kooperationen\nSocial Links und Partneranfragen werden vom Admin-Team gepflegt und koordiniert.", + ContactUrl = "https://vtuber-star-awards.de/kontakt", + DemoLoginDisplayName = "Jayuhime Admin", + DemoLoginEmail = "", + DemoLoginEnabled = false, + DemoLoginManagedByDatabase = false, + DemoLoginPasswordHash = "", + DemoLoginPasswordSalt = "", + DemoLoginTwitchUserId = "jayuhime_admin", + FaqJson = "[{\"question\":\"Wer darf nominiert werden?\",\"answer\":\"Jede:r aktive deutschsprachige VTuber kann nominiert werden \\u2014 unabh\\u00E4ngig von Follower-Zahl oder Plattform. Die Community schl\\u00E4gt in der Nominierungsphase ihre Favorit:innen vor.\"},{\"question\":\"Wie funktioniert das Voting?\",\"answer\":\"Du meldest dich ausschlie\\u00DFlich mit deinem Twitch-Account an \\u2014 nur so kannst du teilnehmen. Das h\\u00E4lt das Voting fair und bot-frei. Danach vergibst du pro Kategorie eine Stimme, \\u00E4nderbar bis zum Ende der Phase.\"},{\"question\":\"Was kostet die Teilnahme?\",\"answer\":\"Gar nichts. Nominieren, voten und die Show schauen ist komplett kostenlos \\u2014 der VTuber Star Award ist ein Community-Event von Fans f\\u00FCr Fans.\"},{\"question\":\"Wann und wo findet die Award-Show statt?\",\"answer\":\"Die gro\\u00DFe Live-Show wird von Jayuhime gehostet und auf Twitch \\u0026 YouTube gestreamt. Den genauen Termin findest du im Countdown oben \\u2014 sei live dabei, wenn die Stars gek\\u00FCrt werden!\"},{\"question\":\"Ich wurde nominiert \\u2014 was nun?\",\"answer\":\"Gl\\u00FCckwunsch! Du erh\\u00E4ltst eine Benachrichtigung mit allen Infos. Teile deine Nominierung gern mit deiner Community und ruf zum Voten auf \\u2014 jede Stimme z\\u00E4hlt.\"}]", + HostDisplayName = "Jayuhime", + HostTagline = "VTuber & Award Host", + ImprintContent = "Anbieter\nVTuber Star Awards, vertreten durch Jayuhime.\n\nKontakt\nNutze fuer organisatorische Fragen bitte die Kontaktseite oder die hinterlegte Kontaktadresse.\n\nHinweis\nDieses Impressum ist ein redaktioneller Platzhalter und sollte vor dem Livegang mit den tatsaechlichen Anbieterangaben ersetzt werden.", + ImprintUrl = "https://vtuber-star-awards.de/impressum", + MaintenanceMessage = "Die Award-Galaxie wird gerade liebevoll poliert. Schau gleich wieder vorbei.", + MaintenanceModeEnabled = false, + MaintenanceTitle = "Sternenpause", + NewsletterUrl = "https://vtuber-star-awards.de/newsletter", + NominationLinkBlacklistJson = "[{\"Url\":\"https://kick.com/\"},{\"Url\":\"https://www.twitch.tv/\"},{\"Url\":\"https://www.youtube.com/\"}]", + PrivacyEmail = "datenschutz@vtuber-star-awards.de", + PrivacyPolicyContent = "Verantwortliche:r\nVTuber Star Awards, vertreten durch Jayuhime. Kontakt: datenschutz@vtuber-star-awards.de\n\nWelche Daten wir verarbeiten\nBei Teilnahme (Voting, Nominierung, Clip-Einreichung) verarbeiten wir ausschließlich deine Twitch-User-ID sowie den Zeitstempel deiner Aktion.\nFür Show-Erinnerungen speichern wir optional deine E-Mail-Adresse.\n\nRechtsgrundlage\nVerarbeitung auf Basis von Art. 6 Abs. 1 lit. b DSGVO. Für optionale Erinnerungen gilt Art. 6 Abs. 1 lit. a DSGVO.\n\nZweck der Verarbeitung\nDurchführung des VTuber Star Awards, Sicherstellung fairer Abstimmung, Spam-Prävention sowie Admin-Review von Einreichungen.\n\nLöschfristen\nAlle Teilnahmedaten werden spätestens 6 Monate nach der Award-Show automatisch gelöscht. E-Mail-Adressen werden nach der Show entfernt.\n\nDeine Rechte\nDu hast das Recht auf Auskunft, Löschung, Berichtigung, Widerspruch und Beschwerde bei der zuständigen Aufsichtsbehörde.\n\nWeitergabe an Dritte\nKeine Weitergabe zu Werbezwecken. Technische Dienstleister verarbeiten Daten ausschließlich im Rahmen der Auftragsverarbeitung.", + PrivacyPolicyUpdatedAt = new DateTimeOffset(new DateTime(2026, 6, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + PrivacyPolicyUpdatedBy = "seed", + RiskRulesJson = "[{\"key\":\"resubmitted_ballot\",\"label\":\"Ballot erneut gespeichert\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"low\",\"description\":\"Erstellt einen Hinweis, wenn ein User sein Voting erneut speichert.\"},{\"key\":\"rapid_vote_updates\",\"label\":\"Voting-Burst\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr schnell mehrere Voting-Aenderungen ausloest.\"},{\"key\":\"resubmitted_nomination\",\"label\":\"Nominierung erneut eingereicht\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"low\",\"description\":\"Erstellt einen Hinweis, wenn ein User in derselben Kategorie erneut nominiert.\"},{\"key\":\"rapid_nomination_burst\",\"label\":\"Nominierungs-Burst\",\"enabled\":true,\"threshold\":10,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr viele Nominierungen in kurzer Zeit sendet.\"},{\"key\":\"duplicate_clip_submission\",\"label\":\"Doppelter Clip\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn ein User denselben Clip erneut einreicht.\"},{\"key\":\"rapid_clip_burst\",\"label\":\"Clip-Burst\",\"enabled\":true,\"threshold\":5,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr viele Clips in kurzer Zeit sendet.\"},{\"key\":\"rapid_login_ip\",\"label\":\"Login-Burst pro IP\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":15,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn mehrere neue Sessions von derselben IP entstehen.\"},{\"key\":\"rapid_demo_login_ip\",\"label\":\"Demo-Login-Burst pro IP\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":15,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn mehrere Demo-Admin-Sessions von derselben IP entstehen.\"}]", + ShareDiscordUrl = "", + ShareXUrl = "", + ShowactApplicationDisabledMessage = "Showact-Bewerbungen sind aktuell geschlossen.", + ShowactApplicationsEnabled = false, + ShowactFormSchemaJson = "[]", + ShowactsContent = "Informationen zu Showact-Bewerbungen, Ablauf und Kontakt fuer die Award-Show.", + ShowactsUrl = "https://vtuber-star-awards.de/showacts", + SocialLinksJson = "[{\"label\":\"Twitch\",\"platform\":\"twitch\",\"url\":\"https://twitch.tv/jayuhime\",\"icon\":\"twitch\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"YouTube\",\"platform\":\"youtube\",\"url\":\"https://youtube.com/c/Jayuhime\",\"icon\":\"youtube\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"X\",\"platform\":\"x\",\"url\":\"https://x.com/jayuhime\",\"icon\":\"x\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Instagram\",\"platform\":\"instagram\",\"url\":\"https://instagram.com/jayuhime\",\"icon\":\"instagram\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Discord\",\"platform\":\"discord\",\"url\":\"https://discord.gg/jayuhime\",\"icon\":\"discord\",\"showOnHost\":true,\"showOnCommunity\":true}]", + SponsorsContent = "Sponsoren & Partner\nHier koennen Sponsor:innen, Medienpartner, Community-Partner und Supporter des VTuber Star Awards vorgestellt werden.\n\nPartner werden im Rahmen der Show und auf den oeffentlichen Kontaktflaechen genannt, sobald sie final bestaetigt sind.", + SponsorsUrl = "https://vtuber-star-awards.de/partner", + SponsorsVisible = true, + TwitchAuthManagedByDatabase = false, + TwitchClientId = "", + TwitchClientSecret = "", + TwitchRedirectUri = "", + TwitchScope = "", + WorkflowRulesJson = "[{\"key\":\"max_finalists_per_category\",\"label\":\"Finale Kandidat:innen pro Kategorie\",\"enabled\":true,\"limit\":4,\"mode\":\"block\",\"description\":\"Begrenzt, wie viele finale Kandidat:innen in einer Kategorie vorbereitet werden.\"},{\"key\":\"max_candidate_appearances\",\"label\":\"Kandidaturen pro Person\",\"enabled\":true,\"limit\":2,\"mode\":\"warn\",\"description\":\"Warnt oder blockiert, wenn dieselbe Person in zu vielen Kategorien final auftaucht.\"},{\"key\":\"max_winner_placements\",\"label\":\"Gewinnerpl\\u00E4tze pro Person\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn dieselbe Person mehrfach als Gewinner:in gesetzt wird.\"},{\"key\":\"winner_requires_clip\",\"label\":\"Gewinner braucht Clip-Link\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn ein Gewinner ohne gepflegten YouTube-/Twitch-Clip gesetzt wird.\"}]" + }); + }); + + modelBuilder.Entity("Backend.Domain.Sponsor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsVisible") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tier") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WebsiteUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "IsVisible", "SortOrder"); + + b.ToTable("Sponsors"); + }); + + modelBuilder.Entity("Backend.Domain.TeamMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoundTwitchDisplayName") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("BoundTwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Login") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MustChangePassword") + .HasColumnType("boolean"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("PasswordResetAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("TwitchBoundAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("BoundTwitchUserId") + .IsUnique(); + + b.HasIndex("Login") + .IsUnique(); + + b.ToTable("TeamMembers"); + }); + + modelBuilder.Entity("Backend.Domain.TeamRolePermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PermissionsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("Role") + .IsUnique(); + + b.ToTable("TeamRolePermissions"); + }); + + modelBuilder.Entity("Backend.Domain.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SessionToken") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SessionToken") + .IsUnique(); + + b.ToTable("UserSessions"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "SubmittedByTwitchId") + .IsUnique(); + + b.ToTable("VoteBallots"); + + b.HasData( + new + { + Id = 1, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_1" + }, + new + { + Id = 2, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 5, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_2" + }); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BallotId") + .HasColumnType("integer"); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BallotId"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.ToTable("VoteEntries"); + + b.HasData( + new + { + Id = 1, + BallotId = 1, + CandidateId = 1, + CategoryId = 1 + }, + new + { + Id = 2, + BallotId = 1, + CandidateId = 4, + CategoryId = 2 + }, + new + { + Id = 3, + BallotId = 2, + CandidateId = 2, + CategoryId = 1 + }, + new + { + Id = 4, + BallotId = 2, + CandidateId = 6, + CategoryId = 3 + }); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Results") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.HasOne("Backend.Domain.Category", "Category") + .WithMany("Candidates") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Categories") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Candidate"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId"); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.ShowactApplication", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Sponsor", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.HasOne("Backend.Domain.VoteBallot", "Ballot") + .WithMany("Entries") + .HasForeignKey("BallotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ballot"); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Navigation("Candidates"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Navigation("Categories"); + + b.Navigation("Results"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Navigation("Entries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/Migrations/20260628091734_AddCategoryViewerRanges.cs b/Backend/Migrations/20260628091734_AddCategoryViewerRanges.cs new file mode 100644 index 0000000..bec5c66 --- /dev/null +++ b/Backend/Migrations/20260628091734_AddCategoryViewerRanges.cs @@ -0,0 +1,108 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backend.Migrations +{ + /// + public partial class AddCategoryViewerRanges : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ViewerRangeMax", + table: "Categories", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "ViewerRangeMin", + table: "Categories", + type: "integer", + nullable: true); + + migrationBuilder.UpdateData( + table: "Categories", + keyColumn: "Id", + keyValue: 1, + columns: new[] { "ViewerRangeMax", "ViewerRangeMin" }, + values: new object[] { null, null }); + + migrationBuilder.UpdateData( + table: "Categories", + keyColumn: "Id", + keyValue: 2, + columns: new[] { "ViewerRangeMax", "ViewerRangeMin" }, + values: new object[] { null, null }); + + migrationBuilder.UpdateData( + table: "Categories", + keyColumn: "Id", + keyValue: 3, + columns: new[] { "ViewerRangeMax", "ViewerRangeMin" }, + values: new object[] { null, null }); + + migrationBuilder.UpdateData( + table: "Categories", + keyColumn: "Id", + keyValue: 4, + columns: new[] { "ViewerRangeMax", "ViewerRangeMin" }, + values: new object[] { null, null }); + + migrationBuilder.UpdateData( + table: "Categories", + keyColumn: "Id", + keyValue: 5, + columns: new[] { "ViewerRangeMax", "ViewerRangeMin" }, + values: new object[] { null, null }); + + migrationBuilder.UpdateData( + table: "Categories", + keyColumn: "Id", + keyValue: 6, + columns: new[] { "ViewerRangeMax", "ViewerRangeMin" }, + values: new object[] { null, null }); + + migrationBuilder.UpdateData( + table: "Categories", + keyColumn: "Id", + keyValue: 7, + columns: new[] { "ViewerRangeMax", "ViewerRangeMin" }, + values: new object[] { null, null }); + + migrationBuilder.UpdateData( + table: "Categories", + keyColumn: "Id", + keyValue: 8, + columns: new[] { "ViewerRangeMax", "ViewerRangeMin" }, + values: new object[] { null, null }); + + migrationBuilder.UpdateData( + table: "Categories", + keyColumn: "Id", + keyValue: 9, + columns: new[] { "ViewerRangeMax", "ViewerRangeMin" }, + values: new object[] { null, null }); + + migrationBuilder.UpdateData( + table: "Categories", + keyColumn: "Id", + keyValue: 10, + columns: new[] { "ViewerRangeMax", "ViewerRangeMin" }, + values: new object[] { null, null }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ViewerRangeMax", + table: "Categories"); + + migrationBuilder.DropColumn( + name: "ViewerRangeMin", + table: "Categories"); + } + } +} diff --git a/Backend/Migrations/20260628092717_AddSessionIdleTimeoutSettings.Designer.cs b/Backend/Migrations/20260628092717_AddSessionIdleTimeoutSettings.Designer.cs new file mode 100644 index 0000000..dc8953d --- /dev/null +++ b/Backend/Migrations/20260628092717_AddSessionIdleTimeoutSettings.Designer.cs @@ -0,0 +1,1789 @@ +// +using System; +using Backend.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backend.Migrations +{ + [DbContext(typeof(AwardsDbContext))] + [Migration("20260628092717_AddSessionIdleTimeoutSettings")] + partial class AddSessionIdleTimeoutSettings + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backend.Domain.AdminAuditEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("AdminTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.ToTable("AdminAuditEntries"); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CategoryName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "CategoryId") + .IsUnique(); + + b.ToTable("Results"); + + b.HasData( + new + { + Id = 1, + CandidateId = 8, + CategoryId = 5, + CategoryName = "VTuber des Jahres", + SeasonId = 2 + }, + new + { + Id = 2, + CandidateId = 9, + CategoryId = 6, + CategoryName = "Bestes Live Event", + SeasonId = 2 + }, + new + { + Id = 3, + CandidateId = 10, + CategoryId = 7, + CategoryName = "Clip des Jahres", + SeasonId = 2 + }, + new + { + Id = 4, + CandidateId = 11, + CategoryId = 8, + CategoryName = "VTuber des Jahres", + SeasonId = 3 + }, + new + { + Id = 5, + CandidateId = 12, + CategoryId = 9, + CategoryName = "Clip des Jahres", + SeasonId = 3 + }, + new + { + Id = 6, + CandidateId = 13, + CategoryId = 10, + CategoryName = "VTuber des Jahres", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AcceptanceNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AcceptanceStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasDefaultValue("open"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ChannelSlug") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ClipCompilationPlatform") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ClipCompilationTitle") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ClipCompilationUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ClipEmbedStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasDefaultValue("unchecked"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId"); + + b.ToTable("Candidates"); + + b.HasData( + new + { + Id = 1, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@hoshimimiyu", + ClipEmbedStatus = "unchecked", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 2, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 3, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@shiroch", + ClipEmbedStatus = "unchecked", + DisplayName = "Shiro Ch.", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 4, + AcceptanceStatus = "open", + CategoryId = 2, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 5, + AcceptanceStatus = "open", + CategoryId = 2, + ChannelSlug = "@aoisakura", + ClipEmbedStatus = "unchecked", + DisplayName = "Aoi Sakura Showcase", + Platform = "YouTube", + SeasonId = 1 + }, + new + { + Id = 6, + AcceptanceStatus = "open", + CategoryId = 3, + ChannelSlug = "@pyonkichikingdom", + ClipEmbedStatus = "unchecked", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 7, + AcceptanceStatus = "open", + CategoryId = 4, + ChannelSlug = "@moonrelay", + ClipEmbedStatus = "unchecked", + DisplayName = "Moonrelay", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 8, + AcceptanceStatus = "open", + CategoryId = 5, + ChannelSlug = "@hoshimimiyu", + ClipEmbedStatus = "unchecked", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 9, + AcceptanceStatus = "open", + CategoryId = 6, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 10, + AcceptanceStatus = "open", + CategoryId = 7, + ChannelSlug = "@pyonkichikingdom", + ClipEmbedStatus = "unchecked", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 11, + AcceptanceStatus = "open", + CategoryId = 8, + ChannelSlug = "@aoisakura", + ClipEmbedStatus = "unchecked", + DisplayName = "Aoi Sakura", + Platform = "YouTube", + SeasonId = 3 + }, + new + { + Id = 12, + AcceptanceStatus = "open", + CategoryId = 9, + ChannelSlug = "@starbyte", + ClipEmbedStatus = "unchecked", + DisplayName = "Starbyte", + Platform = "Twitch", + SeasonId = 3 + }, + new + { + Id = 13, + AcceptanceStatus = "open", + CategoryId = 10, + ChannelSlug = "@tenshivox", + ClipEmbedStatus = "unchecked", + DisplayName = "Tenshi Vox", + Platform = "Twitch", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MaxNomineesPerUser") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("ViewerRangeMax") + .HasColumnType("integer"); + + b.Property("ViewerRangeMin") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Slug") + .IsUnique(); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = 1, + Description = "Die groesste Auszeichnung des Jahres.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 1, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 2, + Description = "Events, Konzerte und 3D-Shows.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 1, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 3, + Description = "Der lustigste oder emotionalste Clip des Jahres.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 1, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 4, + Description = "Die aktivste und freundlichste Community.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "Beste Community", + SeasonId = 1, + Slug = "beste-community", + SortOrder = 4 + }, + new + { + Id = 5, + Description = "Archivkategorie 2025.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 2, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 6, + Description = "Archivkategorie 2025.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 2, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 7, + Description = "Archivkategorie 2025.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 2, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 8, + Description = "Archivkategorie 2024.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 3, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 9, + Description = "Archivkategorie 2024.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 3, + Slug = "clip-des-jahres", + SortOrder = 2 + }, + new + { + Id = 10, + Description = "Archivkategorie 2023.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 4, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ClipUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Creator") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("ClipSubmissions"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CandidateText") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("StreamUrl") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("Nominations"); + + b.HasData( + new + { + Id = 1, + CandidateText = "Hoshimi Miyu", + CategoryId = 1, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 13, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_hoshi" + }, + new + { + Id = 2, + CandidateText = "Kurainu 3D Live", + CategoryId = 2, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 14, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_kurainu" + }); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("TwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("RiskFlags"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrentPhase") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("IsCommunityOnly") + .HasColumnType("boolean"); + + b.Property("IsCurrent") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("NominationEndsAt") + .HasColumnType("date"); + + b.Property("NominationStartsAt") + .HasColumnType("date"); + + b.Property("ReviewEndsAt") + .HasColumnType("date"); + + b.Property("ReviewStartsAt") + .HasColumnType("date"); + + b.Property("ShowDate") + .HasColumnType("date"); + + b.Property("ShowStartsAt") + .HasColumnType("time without time zone"); + + b.Property("ShowStreamUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("VotingEndsAt") + .HasColumnType("date"); + + b.Property("VotingStartsAt") + .HasColumnType("date"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("Seasons"); + + b.HasData( + new + { + Id = 1, + CurrentPhase = "Community Voting", + IsCommunityOnly = true, + IsCurrent = true, + Name = "VTuber Star Awards 2026", + NominationEndsAt = new DateOnly(2026, 5, 31), + NominationStartsAt = new DateOnly(2026, 5, 1), + ReviewEndsAt = new DateOnly(2026, 7, 10), + ReviewStartsAt = new DateOnly(2026, 7, 1), + ShowDate = new DateOnly(2026, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2026, 6, 30), + VotingStartsAt = new DateOnly(2026, 6, 1), + Year = 2026 + }, + new + { + Id = 2, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2025", + NominationEndsAt = new DateOnly(2025, 5, 31), + NominationStartsAt = new DateOnly(2025, 5, 1), + ReviewEndsAt = new DateOnly(2025, 7, 10), + ReviewStartsAt = new DateOnly(2025, 7, 1), + ShowDate = new DateOnly(2025, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2025, 6, 30), + VotingStartsAt = new DateOnly(2025, 6, 1), + Year = 2025 + }, + new + { + Id = 3, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2024", + NominationEndsAt = new DateOnly(2024, 5, 31), + NominationStartsAt = new DateOnly(2024, 5, 1), + ReviewEndsAt = new DateOnly(2024, 7, 10), + ReviewStartsAt = new DateOnly(2024, 7, 1), + ShowDate = new DateOnly(2024, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://youtube.com/c/Jayuhime", + VotingEndsAt = new DateOnly(2024, 6, 30), + VotingStartsAt = new DateOnly(2024, 6, 1), + Year = 2024 + }, + new + { + Id = 4, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2023", + NominationEndsAt = new DateOnly(2023, 5, 31), + NominationStartsAt = new DateOnly(2023, 5, 1), + ReviewEndsAt = new DateOnly(2023, 7, 10), + ReviewStartsAt = new DateOnly(2023, 7, 1), + ShowDate = new DateOnly(2023, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + VotingEndsAt = new DateOnly(2023, 6, 30), + VotingStartsAt = new DateOnly(2023, 6, 1), + Year = 2023 + }); + }); + + modelBuilder.Entity("Backend.Domain.ShowactApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ArtistName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ContactDiscord") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ContactEmail") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("FieldResponsesJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("PerformanceType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("PlatformUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReferenceUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TechnicalNotes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("ShowactApplications"); + }); + + modelBuilder.Entity("Backend.Domain.SiteSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClipAdminMenuVisible") + .HasColumnType("boolean"); + + b.Property("ClipReviewEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ClipSubmissionDisabledMessage") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("ClipSubmissionsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ContactContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("DemoLoginDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginEmail") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("DemoLoginEnabled") + .HasColumnType("boolean"); + + b.Property("DemoLoginManagedByDatabase") + .HasColumnType("boolean"); + + b.Property("DemoLoginPasswordHash") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginPasswordSalt") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DemoLoginTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("FaqJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("HostDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("HostTagline") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("ImprintContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("ImprintUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("MaintenanceMessage") + .IsRequired() + .HasMaxLength(600) + .HasColumnType("character varying(600)"); + + b.Property("MaintenanceModeEnabled") + .HasColumnType("boolean"); + + b.Property("MaintenanceTitle") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("NewsletterUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("NominationLinkBlacklistJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("[]"); + + b.Property("PrivacyEmail") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("PrivacyPolicyContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("PrivacyPolicyUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivacyPolicyUpdatedBy") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("RiskRulesJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SessionIdleTimeoutHours") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(3); + + b.Property("ShareDiscordUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShareXUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShowactApplicationDisabledMessage") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("ShowactApplicationsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ShowactFormSchemaJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShowactsContent") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue(""); + + b.Property("ShowactsUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("SocialLinksJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("SponsorsVisible") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("TwitchAuthManagedByDatabase") + .HasColumnType("boolean"); + + b.Property("TwitchClientId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchClientSecret") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("TwitchRedirectUri") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("TwitchScope") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("WorkflowRulesJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("[]"); + + b.HasKey("Id"); + + b.ToTable("SiteSettings"); + + b.HasData( + new + { + Id = 1, + ClipAdminMenuVisible = true, + ClipReviewEnabled = true, + ClipSubmissionDisabledMessage = "Clip-Einreichungen sind aktuell geschlossen.", + ClipSubmissionsEnabled = false, + ContactContent = "Kontakt zum Award-Team\nDu hast Fragen zur Nominierung, zum Voting, zu Clips oder zur Show? Schreib dem Team ueber die hinterlegte Kontaktseite.\n\nDatenschutzfragen\nFuer Datenschutzanfragen nutze bitte die Datenschutz-E-Mail aus diesem Footer.\n\nCommunity & Kooperationen\nSocial Links und Partneranfragen werden vom Admin-Team gepflegt und koordiniert.", + ContactUrl = "https://vtuber-star-awards.de/kontakt", + DemoLoginDisplayName = "Jayuhime Admin", + DemoLoginEmail = "", + DemoLoginEnabled = false, + DemoLoginManagedByDatabase = false, + DemoLoginPasswordHash = "", + DemoLoginPasswordSalt = "", + DemoLoginTwitchUserId = "jayuhime_admin", + FaqJson = "[{\"question\":\"Wer darf nominiert werden?\",\"answer\":\"Jede:r aktive deutschsprachige VTuber kann nominiert werden \\u2014 unabh\\u00E4ngig von Follower-Zahl oder Plattform. Die Community schl\\u00E4gt in der Nominierungsphase ihre Favorit:innen vor.\"},{\"question\":\"Wie funktioniert das Voting?\",\"answer\":\"Du meldest dich ausschlie\\u00DFlich mit deinem Twitch-Account an \\u2014 nur so kannst du teilnehmen. Das h\\u00E4lt das Voting fair und bot-frei. Danach vergibst du pro Kategorie eine Stimme, \\u00E4nderbar bis zum Ende der Phase.\"},{\"question\":\"Was kostet die Teilnahme?\",\"answer\":\"Gar nichts. Nominieren, voten und die Show schauen ist komplett kostenlos \\u2014 der VTuber Star Award ist ein Community-Event von Fans f\\u00FCr Fans.\"},{\"question\":\"Wann und wo findet die Award-Show statt?\",\"answer\":\"Die gro\\u00DFe Live-Show wird von Jayuhime gehostet und auf Twitch \\u0026 YouTube gestreamt. Den genauen Termin findest du im Countdown oben \\u2014 sei live dabei, wenn die Stars gek\\u00FCrt werden!\"},{\"question\":\"Ich wurde nominiert \\u2014 was nun?\",\"answer\":\"Gl\\u00FCckwunsch! Du erh\\u00E4ltst eine Benachrichtigung mit allen Infos. Teile deine Nominierung gern mit deiner Community und ruf zum Voten auf \\u2014 jede Stimme z\\u00E4hlt.\"}]", + HostDisplayName = "Jayuhime", + HostTagline = "VTuber & Award Host", + ImprintContent = "Anbieter\nVTuber Star Awards, vertreten durch Jayuhime.\n\nKontakt\nNutze fuer organisatorische Fragen bitte die Kontaktseite oder die hinterlegte Kontaktadresse.\n\nHinweis\nDieses Impressum ist ein redaktioneller Platzhalter und sollte vor dem Livegang mit den tatsaechlichen Anbieterangaben ersetzt werden.", + ImprintUrl = "https://vtuber-star-awards.de/impressum", + MaintenanceMessage = "Die Award-Galaxie wird gerade liebevoll poliert. Schau gleich wieder vorbei.", + MaintenanceModeEnabled = false, + MaintenanceTitle = "Sternenpause", + NewsletterUrl = "https://vtuber-star-awards.de/newsletter", + NominationLinkBlacklistJson = "[{\"Url\":\"https://kick.com/\"},{\"Url\":\"https://www.twitch.tv/\"},{\"Url\":\"https://www.youtube.com/\"}]", + PrivacyEmail = "datenschutz@vtuber-star-awards.de", + PrivacyPolicyContent = "Verantwortliche:r\nVTuber Star Awards, vertreten durch Jayuhime. Kontakt: datenschutz@vtuber-star-awards.de\n\nWelche Daten wir verarbeiten\nBei Teilnahme (Voting, Nominierung, Clip-Einreichung) verarbeiten wir ausschließlich deine Twitch-User-ID sowie den Zeitstempel deiner Aktion.\nFür Show-Erinnerungen speichern wir optional deine E-Mail-Adresse.\n\nRechtsgrundlage\nVerarbeitung auf Basis von Art. 6 Abs. 1 lit. b DSGVO. Für optionale Erinnerungen gilt Art. 6 Abs. 1 lit. a DSGVO.\n\nZweck der Verarbeitung\nDurchführung des VTuber Star Awards, Sicherstellung fairer Abstimmung, Spam-Prävention sowie Admin-Review von Einreichungen.\n\nLöschfristen\nAlle Teilnahmedaten werden spätestens 6 Monate nach der Award-Show automatisch gelöscht. E-Mail-Adressen werden nach der Show entfernt.\n\nDeine Rechte\nDu hast das Recht auf Auskunft, Löschung, Berichtigung, Widerspruch und Beschwerde bei der zuständigen Aufsichtsbehörde.\n\nWeitergabe an Dritte\nKeine Weitergabe zu Werbezwecken. Technische Dienstleister verarbeiten Daten ausschließlich im Rahmen der Auftragsverarbeitung.", + PrivacyPolicyUpdatedAt = new DateTimeOffset(new DateTime(2026, 6, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + PrivacyPolicyUpdatedBy = "seed", + RiskRulesJson = "[{\"key\":\"resubmitted_ballot\",\"label\":\"Ballot erneut gespeichert\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"low\",\"description\":\"Erstellt einen Hinweis, wenn ein User sein Voting erneut speichert.\"},{\"key\":\"rapid_vote_updates\",\"label\":\"Voting-Burst\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr schnell mehrere Voting-Aenderungen ausloest.\"},{\"key\":\"resubmitted_nomination\",\"label\":\"Nominierung erneut eingereicht\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"low\",\"description\":\"Erstellt einen Hinweis, wenn ein User in derselben Kategorie erneut nominiert.\"},{\"key\":\"rapid_nomination_burst\",\"label\":\"Nominierungs-Burst\",\"enabled\":true,\"threshold\":10,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr viele Nominierungen in kurzer Zeit sendet.\"},{\"key\":\"duplicate_clip_submission\",\"label\":\"Doppelter Clip\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn ein User denselben Clip erneut einreicht.\"},{\"key\":\"rapid_clip_burst\",\"label\":\"Clip-Burst\",\"enabled\":true,\"threshold\":5,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr viele Clips in kurzer Zeit sendet.\"},{\"key\":\"rapid_login_ip\",\"label\":\"Login-Burst pro IP\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":15,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn mehrere neue Sessions von derselben IP entstehen.\"},{\"key\":\"rapid_demo_login_ip\",\"label\":\"Demo-Login-Burst pro IP\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":15,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn mehrere Demo-Admin-Sessions von derselben IP entstehen.\"}]", + SessionIdleTimeoutHours = 3, + ShareDiscordUrl = "", + ShareXUrl = "", + ShowactApplicationDisabledMessage = "Showact-Bewerbungen sind aktuell geschlossen.", + ShowactApplicationsEnabled = false, + ShowactFormSchemaJson = "[]", + ShowactsContent = "Informationen zu Showact-Bewerbungen, Ablauf und Kontakt fuer die Award-Show.", + ShowactsUrl = "https://vtuber-star-awards.de/showacts", + SocialLinksJson = "[{\"label\":\"Twitch\",\"platform\":\"twitch\",\"url\":\"https://twitch.tv/jayuhime\",\"icon\":\"twitch\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"YouTube\",\"platform\":\"youtube\",\"url\":\"https://youtube.com/c/Jayuhime\",\"icon\":\"youtube\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"X\",\"platform\":\"x\",\"url\":\"https://x.com/jayuhime\",\"icon\":\"x\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Instagram\",\"platform\":\"instagram\",\"url\":\"https://instagram.com/jayuhime\",\"icon\":\"instagram\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Discord\",\"platform\":\"discord\",\"url\":\"https://discord.gg/jayuhime\",\"icon\":\"discord\",\"showOnHost\":true,\"showOnCommunity\":true}]", + SponsorsContent = "Sponsoren & Partner\nHier koennen Sponsor:innen, Medienpartner, Community-Partner und Supporter des VTuber Star Awards vorgestellt werden.\n\nPartner werden im Rahmen der Show und auf den oeffentlichen Kontaktflaechen genannt, sobald sie final bestaetigt sind.", + SponsorsUrl = "https://vtuber-star-awards.de/partner", + SponsorsVisible = true, + TwitchAuthManagedByDatabase = false, + TwitchClientId = "", + TwitchClientSecret = "", + TwitchRedirectUri = "", + TwitchScope = "", + WorkflowRulesJson = "[{\"key\":\"max_finalists_per_category\",\"label\":\"Finale Kandidat:innen pro Kategorie\",\"enabled\":true,\"limit\":4,\"mode\":\"block\",\"description\":\"Begrenzt, wie viele finale Kandidat:innen in einer Kategorie vorbereitet werden.\"},{\"key\":\"max_candidate_appearances\",\"label\":\"Kandidaturen pro Person\",\"enabled\":true,\"limit\":2,\"mode\":\"warn\",\"description\":\"Warnt oder blockiert, wenn dieselbe Person in zu vielen Kategorien final auftaucht.\"},{\"key\":\"max_winner_placements\",\"label\":\"Gewinnerpl\\u00E4tze pro Person\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn dieselbe Person mehrfach als Gewinner:in gesetzt wird.\"},{\"key\":\"winner_requires_clip\",\"label\":\"Gewinner braucht Clip-Link\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn ein Gewinner ohne gepflegten YouTube-/Twitch-Clip gesetzt wird.\"}]" + }); + }); + + modelBuilder.Entity("Backend.Domain.Sponsor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsVisible") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tier") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WebsiteUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "IsVisible", "SortOrder"); + + b.ToTable("Sponsors"); + }); + + modelBuilder.Entity("Backend.Domain.TeamMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoundTwitchDisplayName") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("BoundTwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Login") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MustChangePassword") + .HasColumnType("boolean"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("PasswordResetAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("TwitchBoundAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("BoundTwitchUserId") + .IsUnique(); + + b.HasIndex("Login") + .IsUnique(); + + b.ToTable("TeamMembers"); + }); + + modelBuilder.Entity("Backend.Domain.TeamRolePermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PermissionsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("Role") + .IsUnique(); + + b.ToTable("TeamRolePermissions"); + }); + + modelBuilder.Entity("Backend.Domain.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SessionToken") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SessionToken") + .IsUnique(); + + b.ToTable("UserSessions"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "SubmittedByTwitchId") + .IsUnique(); + + b.ToTable("VoteBallots"); + + b.HasData( + new + { + Id = 1, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_1" + }, + new + { + Id = 2, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 5, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_2" + }); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BallotId") + .HasColumnType("integer"); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BallotId"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.ToTable("VoteEntries"); + + b.HasData( + new + { + Id = 1, + BallotId = 1, + CandidateId = 1, + CategoryId = 1 + }, + new + { + Id = 2, + BallotId = 1, + CandidateId = 4, + CategoryId = 2 + }, + new + { + Id = 3, + BallotId = 2, + CandidateId = 2, + CategoryId = 1 + }, + new + { + Id = 4, + BallotId = 2, + CandidateId = 6, + CategoryId = 3 + }); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Results") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.HasOne("Backend.Domain.Category", "Category") + .WithMany("Candidates") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Categories") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Candidate"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId"); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.ShowactApplication", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Sponsor", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.HasOne("Backend.Domain.VoteBallot", "Ballot") + .WithMany("Entries") + .HasForeignKey("BallotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ballot"); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Navigation("Candidates"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Navigation("Categories"); + + b.Navigation("Results"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Navigation("Entries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/Migrations/20260628092717_AddSessionIdleTimeoutSettings.cs b/Backend/Migrations/20260628092717_AddSessionIdleTimeoutSettings.cs new file mode 100644 index 0000000..333d2f3 --- /dev/null +++ b/Backend/Migrations/20260628092717_AddSessionIdleTimeoutSettings.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backend.Migrations +{ + /// + public partial class AddSessionIdleTimeoutSettings : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SessionIdleTimeoutHours", + table: "SiteSettings", + type: "integer", + nullable: false, + defaultValue: 3); + + migrationBuilder.UpdateData( + table: "SiteSettings", + keyColumn: "Id", + keyValue: 1, + column: "SessionIdleTimeoutHours", + value: 3); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SessionIdleTimeoutHours", + table: "SiteSettings"); + } + } +} diff --git a/Backend/Migrations/20260628110302_AddSeasonSubcategoryTemplates.Designer.cs b/Backend/Migrations/20260628110302_AddSeasonSubcategoryTemplates.Designer.cs new file mode 100644 index 0000000..648d75c --- /dev/null +++ b/Backend/Migrations/20260628110302_AddSeasonSubcategoryTemplates.Designer.cs @@ -0,0 +1,1799 @@ +// +using System; +using Backend.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backend.Migrations +{ + [DbContext(typeof(AwardsDbContext))] + [Migration("20260628110302_AddSeasonSubcategoryTemplates")] + partial class AddSeasonSubcategoryTemplates + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backend.Domain.AdminAuditEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("AdminTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.ToTable("AdminAuditEntries"); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CategoryName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "CategoryId") + .IsUnique(); + + b.ToTable("Results"); + + b.HasData( + new + { + Id = 1, + CandidateId = 8, + CategoryId = 5, + CategoryName = "VTuber des Jahres", + SeasonId = 2 + }, + new + { + Id = 2, + CandidateId = 9, + CategoryId = 6, + CategoryName = "Bestes Live Event", + SeasonId = 2 + }, + new + { + Id = 3, + CandidateId = 10, + CategoryId = 7, + CategoryName = "Clip des Jahres", + SeasonId = 2 + }, + new + { + Id = 4, + CandidateId = 11, + CategoryId = 8, + CategoryName = "VTuber des Jahres", + SeasonId = 3 + }, + new + { + Id = 5, + CandidateId = 12, + CategoryId = 9, + CategoryName = "Clip des Jahres", + SeasonId = 3 + }, + new + { + Id = 6, + CandidateId = 13, + CategoryId = 10, + CategoryName = "VTuber des Jahres", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AcceptanceNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AcceptanceStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasDefaultValue("open"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ChannelSlug") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ClipCompilationPlatform") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ClipCompilationTitle") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ClipCompilationUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ClipEmbedStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasDefaultValue("unchecked"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId"); + + b.ToTable("Candidates"); + + b.HasData( + new + { + Id = 1, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@hoshimimiyu", + ClipEmbedStatus = "unchecked", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 2, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 3, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@shiroch", + ClipEmbedStatus = "unchecked", + DisplayName = "Shiro Ch.", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 4, + AcceptanceStatus = "open", + CategoryId = 2, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 5, + AcceptanceStatus = "open", + CategoryId = 2, + ChannelSlug = "@aoisakura", + ClipEmbedStatus = "unchecked", + DisplayName = "Aoi Sakura Showcase", + Platform = "YouTube", + SeasonId = 1 + }, + new + { + Id = 6, + AcceptanceStatus = "open", + CategoryId = 3, + ChannelSlug = "@pyonkichikingdom", + ClipEmbedStatus = "unchecked", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 7, + AcceptanceStatus = "open", + CategoryId = 4, + ChannelSlug = "@moonrelay", + ClipEmbedStatus = "unchecked", + DisplayName = "Moonrelay", + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 8, + AcceptanceStatus = "open", + CategoryId = 5, + ChannelSlug = "@hoshimimiyu", + ClipEmbedStatus = "unchecked", + DisplayName = "Hoshimi Miyu", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 9, + AcceptanceStatus = "open", + CategoryId = 6, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu 3D Live", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 10, + AcceptanceStatus = "open", + CategoryId = 7, + ChannelSlug = "@pyonkichikingdom", + ClipEmbedStatus = "unchecked", + DisplayName = "Pyonkichi Kingdom", + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 11, + AcceptanceStatus = "open", + CategoryId = 8, + ChannelSlug = "@aoisakura", + ClipEmbedStatus = "unchecked", + DisplayName = "Aoi Sakura", + Platform = "YouTube", + SeasonId = 3 + }, + new + { + Id = 12, + AcceptanceStatus = "open", + CategoryId = 9, + ChannelSlug = "@starbyte", + ClipEmbedStatus = "unchecked", + DisplayName = "Starbyte", + Platform = "Twitch", + SeasonId = 3 + }, + new + { + Id = 13, + AcceptanceStatus = "open", + CategoryId = 10, + ChannelSlug = "@tenshivox", + ClipEmbedStatus = "unchecked", + DisplayName = "Tenshi Vox", + Platform = "Twitch", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MaxNomineesPerUser") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("ViewerRangeMax") + .HasColumnType("integer"); + + b.Property("ViewerRangeMin") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Slug") + .IsUnique(); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = 1, + Description = "Die groesste Auszeichnung des Jahres.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 1, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 2, + Description = "Events, Konzerte und 3D-Shows.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 1, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 3, + Description = "Der lustigste oder emotionalste Clip des Jahres.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 1, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 4, + Description = "Die aktivste und freundlichste Community.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "Beste Community", + SeasonId = 1, + Slug = "beste-community", + SortOrder = 4 + }, + new + { + Id = 5, + Description = "Archivkategorie 2025.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 2, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 6, + Description = "Archivkategorie 2025.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 2, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 7, + Description = "Archivkategorie 2025.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 2, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 8, + Description = "Archivkategorie 2024.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 3, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 9, + Description = "Archivkategorie 2024.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 3, + Slug = "clip-des-jahres", + SortOrder = 2 + }, + new + { + Id = 10, + Description = "Archivkategorie 2023.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 4, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ClipUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Creator") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("ClipSubmissions"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CandidateText") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("StreamUrl") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("Nominations"); + + b.HasData( + new + { + Id = 1, + CandidateText = "Hoshimi Miyu", + CategoryId = 1, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 13, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_hoshi" + }, + new + { + Id = 2, + CandidateText = "Kurainu 3D Live", + CategoryId = 2, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 14, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_kurainu" + }); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("TwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("RiskFlags"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrentPhase") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("IsCommunityOnly") + .HasColumnType("boolean"); + + b.Property("IsCurrent") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("NominationEndsAt") + .HasColumnType("date"); + + b.Property("NominationStartsAt") + .HasColumnType("date"); + + b.Property("ReviewEndsAt") + .HasColumnType("date"); + + b.Property("ReviewStartsAt") + .HasColumnType("date"); + + b.Property("ShowDate") + .HasColumnType("date"); + + b.Property("ShowStartsAt") + .HasColumnType("time without time zone"); + + b.Property("ShowStreamUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("SubcategoryTemplatesJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("[]"); + + b.Property("VotingEndsAt") + .HasColumnType("date"); + + b.Property("VotingStartsAt") + .HasColumnType("date"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("Seasons"); + + b.HasData( + new + { + Id = 1, + CurrentPhase = "Community Voting", + IsCommunityOnly = true, + IsCurrent = true, + Name = "VTuber Star Awards 2026", + NominationEndsAt = new DateOnly(2026, 5, 31), + NominationStartsAt = new DateOnly(2026, 5, 1), + ReviewEndsAt = new DateOnly(2026, 7, 10), + ReviewStartsAt = new DateOnly(2026, 7, 1), + ShowDate = new DateOnly(2026, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + SubcategoryTemplatesJson = "[]", + VotingEndsAt = new DateOnly(2026, 6, 30), + VotingStartsAt = new DateOnly(2026, 6, 1), + Year = 2026 + }, + new + { + Id = 2, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2025", + NominationEndsAt = new DateOnly(2025, 5, 31), + NominationStartsAt = new DateOnly(2025, 5, 1), + ReviewEndsAt = new DateOnly(2025, 7, 10), + ReviewStartsAt = new DateOnly(2025, 7, 1), + ShowDate = new DateOnly(2025, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + SubcategoryTemplatesJson = "[]", + VotingEndsAt = new DateOnly(2025, 6, 30), + VotingStartsAt = new DateOnly(2025, 6, 1), + Year = 2025 + }, + new + { + Id = 3, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2024", + NominationEndsAt = new DateOnly(2024, 5, 31), + NominationStartsAt = new DateOnly(2024, 5, 1), + ReviewEndsAt = new DateOnly(2024, 7, 10), + ReviewStartsAt = new DateOnly(2024, 7, 1), + ShowDate = new DateOnly(2024, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://youtube.com/c/Jayuhime", + SubcategoryTemplatesJson = "[]", + VotingEndsAt = new DateOnly(2024, 6, 30), + VotingStartsAt = new DateOnly(2024, 6, 1), + Year = 2024 + }, + new + { + Id = 4, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2023", + NominationEndsAt = new DateOnly(2023, 5, 31), + NominationStartsAt = new DateOnly(2023, 5, 1), + ReviewEndsAt = new DateOnly(2023, 7, 10), + ReviewStartsAt = new DateOnly(2023, 7, 1), + ShowDate = new DateOnly(2023, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + SubcategoryTemplatesJson = "[]", + VotingEndsAt = new DateOnly(2023, 6, 30), + VotingStartsAt = new DateOnly(2023, 6, 1), + Year = 2023 + }); + }); + + modelBuilder.Entity("Backend.Domain.ShowactApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ArtistName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ContactDiscord") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ContactEmail") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("FieldResponsesJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("PerformanceType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("PlatformUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReferenceUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TechnicalNotes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("ShowactApplications"); + }); + + modelBuilder.Entity("Backend.Domain.SiteSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClipAdminMenuVisible") + .HasColumnType("boolean"); + + b.Property("ClipReviewEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ClipSubmissionDisabledMessage") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("ClipSubmissionsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ContactContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("DemoLoginDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginEmail") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("DemoLoginEnabled") + .HasColumnType("boolean"); + + b.Property("DemoLoginManagedByDatabase") + .HasColumnType("boolean"); + + b.Property("DemoLoginPasswordHash") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginPasswordSalt") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DemoLoginTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("FaqJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("HostDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("HostTagline") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("ImprintContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("ImprintUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("MaintenanceMessage") + .IsRequired() + .HasMaxLength(600) + .HasColumnType("character varying(600)"); + + b.Property("MaintenanceModeEnabled") + .HasColumnType("boolean"); + + b.Property("MaintenanceTitle") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("NewsletterUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("NominationLinkBlacklistJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("[]"); + + b.Property("PrivacyEmail") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("PrivacyPolicyContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("PrivacyPolicyUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivacyPolicyUpdatedBy") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("RiskRulesJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SessionIdleTimeoutHours") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(3); + + b.Property("ShareDiscordUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShareXUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShowactApplicationDisabledMessage") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("ShowactApplicationsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ShowactFormSchemaJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShowactsContent") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue(""); + + b.Property("ShowactsUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("SocialLinksJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("SponsorsVisible") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("TwitchAuthManagedByDatabase") + .HasColumnType("boolean"); + + b.Property("TwitchClientId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchClientSecret") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("TwitchRedirectUri") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("TwitchScope") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("WorkflowRulesJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("[]"); + + b.HasKey("Id"); + + b.ToTable("SiteSettings"); + + b.HasData( + new + { + Id = 1, + ClipAdminMenuVisible = true, + ClipReviewEnabled = true, + ClipSubmissionDisabledMessage = "Clip-Einreichungen sind aktuell geschlossen.", + ClipSubmissionsEnabled = false, + ContactContent = "Kontakt zum Award-Team\nDu hast Fragen zur Nominierung, zum Voting, zu Clips oder zur Show? Schreib dem Team ueber die hinterlegte Kontaktseite.\n\nDatenschutzfragen\nFuer Datenschutzanfragen nutze bitte die Datenschutz-E-Mail aus diesem Footer.\n\nCommunity & Kooperationen\nSocial Links und Partneranfragen werden vom Admin-Team gepflegt und koordiniert.", + ContactUrl = "https://vtuber-star-awards.de/kontakt", + DemoLoginDisplayName = "Jayuhime Admin", + DemoLoginEmail = "", + DemoLoginEnabled = false, + DemoLoginManagedByDatabase = false, + DemoLoginPasswordHash = "", + DemoLoginPasswordSalt = "", + DemoLoginTwitchUserId = "jayuhime_admin", + FaqJson = "[{\"question\":\"Wer darf nominiert werden?\",\"answer\":\"Jede:r aktive deutschsprachige VTuber kann nominiert werden \\u2014 unabh\\u00E4ngig von Follower-Zahl oder Plattform. Die Community schl\\u00E4gt in der Nominierungsphase ihre Favorit:innen vor.\"},{\"question\":\"Wie funktioniert das Voting?\",\"answer\":\"Du meldest dich ausschlie\\u00DFlich mit deinem Twitch-Account an \\u2014 nur so kannst du teilnehmen. Das h\\u00E4lt das Voting fair und bot-frei. Danach vergibst du pro Kategorie eine Stimme, \\u00E4nderbar bis zum Ende der Phase.\"},{\"question\":\"Was kostet die Teilnahme?\",\"answer\":\"Gar nichts. Nominieren, voten und die Show schauen ist komplett kostenlos \\u2014 der VTuber Star Award ist ein Community-Event von Fans f\\u00FCr Fans.\"},{\"question\":\"Wann und wo findet die Award-Show statt?\",\"answer\":\"Die gro\\u00DFe Live-Show wird von Jayuhime gehostet und auf Twitch \\u0026 YouTube gestreamt. Den genauen Termin findest du im Countdown oben \\u2014 sei live dabei, wenn die Stars gek\\u00FCrt werden!\"},{\"question\":\"Ich wurde nominiert \\u2014 was nun?\",\"answer\":\"Gl\\u00FCckwunsch! Du erh\\u00E4ltst eine Benachrichtigung mit allen Infos. Teile deine Nominierung gern mit deiner Community und ruf zum Voten auf \\u2014 jede Stimme z\\u00E4hlt.\"}]", + HostDisplayName = "Jayuhime", + HostTagline = "VTuber & Award Host", + ImprintContent = "Anbieter\nVTuber Star Awards, vertreten durch Jayuhime.\n\nKontakt\nNutze fuer organisatorische Fragen bitte die Kontaktseite oder die hinterlegte Kontaktadresse.\n\nHinweis\nDieses Impressum ist ein redaktioneller Platzhalter und sollte vor dem Livegang mit den tatsaechlichen Anbieterangaben ersetzt werden.", + ImprintUrl = "https://vtuber-star-awards.de/impressum", + MaintenanceMessage = "Die Award-Galaxie wird gerade liebevoll poliert. Schau gleich wieder vorbei.", + MaintenanceModeEnabled = false, + MaintenanceTitle = "Sternenpause", + NewsletterUrl = "https://vtuber-star-awards.de/newsletter", + NominationLinkBlacklistJson = "[{\"Url\":\"https://kick.com/\"},{\"Url\":\"https://www.twitch.tv/\"},{\"Url\":\"https://www.youtube.com/\"}]", + PrivacyEmail = "datenschutz@vtuber-star-awards.de", + PrivacyPolicyContent = "Verantwortliche:r\nVTuber Star Awards, vertreten durch Jayuhime. Kontakt: datenschutz@vtuber-star-awards.de\n\nWelche Daten wir verarbeiten\nBei Teilnahme (Voting, Nominierung, Clip-Einreichung) verarbeiten wir ausschließlich deine Twitch-User-ID sowie den Zeitstempel deiner Aktion.\nFür Show-Erinnerungen speichern wir optional deine E-Mail-Adresse.\n\nRechtsgrundlage\nVerarbeitung auf Basis von Art. 6 Abs. 1 lit. b DSGVO. Für optionale Erinnerungen gilt Art. 6 Abs. 1 lit. a DSGVO.\n\nZweck der Verarbeitung\nDurchführung des VTuber Star Awards, Sicherstellung fairer Abstimmung, Spam-Prävention sowie Admin-Review von Einreichungen.\n\nLöschfristen\nAlle Teilnahmedaten werden spätestens 6 Monate nach der Award-Show automatisch gelöscht. E-Mail-Adressen werden nach der Show entfernt.\n\nDeine Rechte\nDu hast das Recht auf Auskunft, Löschung, Berichtigung, Widerspruch und Beschwerde bei der zuständigen Aufsichtsbehörde.\n\nWeitergabe an Dritte\nKeine Weitergabe zu Werbezwecken. Technische Dienstleister verarbeiten Daten ausschließlich im Rahmen der Auftragsverarbeitung.", + PrivacyPolicyUpdatedAt = new DateTimeOffset(new DateTime(2026, 6, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + PrivacyPolicyUpdatedBy = "seed", + RiskRulesJson = "[{\"key\":\"resubmitted_ballot\",\"label\":\"Ballot erneut gespeichert\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"low\",\"description\":\"Erstellt einen Hinweis, wenn ein User sein Voting erneut speichert.\"},{\"key\":\"rapid_vote_updates\",\"label\":\"Voting-Burst\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr schnell mehrere Voting-Aenderungen ausloest.\"},{\"key\":\"resubmitted_nomination\",\"label\":\"Nominierung erneut eingereicht\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"low\",\"description\":\"Erstellt einen Hinweis, wenn ein User in derselben Kategorie erneut nominiert.\"},{\"key\":\"rapid_nomination_burst\",\"label\":\"Nominierungs-Burst\",\"enabled\":true,\"threshold\":10,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr viele Nominierungen in kurzer Zeit sendet.\"},{\"key\":\"duplicate_clip_submission\",\"label\":\"Doppelter Clip\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn ein User denselben Clip erneut einreicht.\"},{\"key\":\"rapid_clip_burst\",\"label\":\"Clip-Burst\",\"enabled\":true,\"threshold\":5,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr viele Clips in kurzer Zeit sendet.\"},{\"key\":\"rapid_login_ip\",\"label\":\"Login-Burst pro IP\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":15,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn mehrere neue Sessions von derselben IP entstehen.\"},{\"key\":\"rapid_demo_login_ip\",\"label\":\"Demo-Login-Burst pro IP\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":15,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn mehrere Demo-Admin-Sessions von derselben IP entstehen.\"}]", + SessionIdleTimeoutHours = 3, + ShareDiscordUrl = "", + ShareXUrl = "", + ShowactApplicationDisabledMessage = "Showact-Bewerbungen sind aktuell geschlossen.", + ShowactApplicationsEnabled = false, + ShowactFormSchemaJson = "[]", + ShowactsContent = "Informationen zu Showact-Bewerbungen, Ablauf und Kontakt fuer die Award-Show.", + ShowactsUrl = "https://vtuber-star-awards.de/showacts", + SocialLinksJson = "[{\"label\":\"Twitch\",\"platform\":\"twitch\",\"url\":\"https://twitch.tv/jayuhime\",\"icon\":\"twitch\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"YouTube\",\"platform\":\"youtube\",\"url\":\"https://youtube.com/c/Jayuhime\",\"icon\":\"youtube\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"X\",\"platform\":\"x\",\"url\":\"https://x.com/jayuhime\",\"icon\":\"x\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Instagram\",\"platform\":\"instagram\",\"url\":\"https://instagram.com/jayuhime\",\"icon\":\"instagram\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Discord\",\"platform\":\"discord\",\"url\":\"https://discord.gg/jayuhime\",\"icon\":\"discord\",\"showOnHost\":true,\"showOnCommunity\":true}]", + SponsorsContent = "Sponsoren & Partner\nHier koennen Sponsor:innen, Medienpartner, Community-Partner und Supporter des VTuber Star Awards vorgestellt werden.\n\nPartner werden im Rahmen der Show und auf den oeffentlichen Kontaktflaechen genannt, sobald sie final bestaetigt sind.", + SponsorsUrl = "https://vtuber-star-awards.de/partner", + SponsorsVisible = true, + TwitchAuthManagedByDatabase = false, + TwitchClientId = "", + TwitchClientSecret = "", + TwitchRedirectUri = "", + TwitchScope = "", + WorkflowRulesJson = "[{\"key\":\"max_finalists_per_category\",\"label\":\"Finale Kandidat:innen pro Kategorie\",\"enabled\":true,\"limit\":4,\"mode\":\"block\",\"description\":\"Begrenzt, wie viele finale Kandidat:innen in einer Kategorie vorbereitet werden.\"},{\"key\":\"max_candidate_appearances\",\"label\":\"Kandidaturen pro Person\",\"enabled\":true,\"limit\":2,\"mode\":\"warn\",\"description\":\"Warnt oder blockiert, wenn dieselbe Person in zu vielen Kategorien final auftaucht.\"},{\"key\":\"max_winner_placements\",\"label\":\"Gewinnerpl\\u00E4tze pro Person\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn dieselbe Person mehrfach als Gewinner:in gesetzt wird.\"},{\"key\":\"winner_requires_clip\",\"label\":\"Gewinner braucht Clip-Link\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn ein Gewinner ohne gepflegten YouTube-/Twitch-Clip gesetzt wird.\"}]" + }); + }); + + modelBuilder.Entity("Backend.Domain.Sponsor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsVisible") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tier") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WebsiteUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "IsVisible", "SortOrder"); + + b.ToTable("Sponsors"); + }); + + modelBuilder.Entity("Backend.Domain.TeamMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoundTwitchDisplayName") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("BoundTwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Login") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MustChangePassword") + .HasColumnType("boolean"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("PasswordResetAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("TwitchBoundAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("BoundTwitchUserId") + .IsUnique(); + + b.HasIndex("Login") + .IsUnique(); + + b.ToTable("TeamMembers"); + }); + + modelBuilder.Entity("Backend.Domain.TeamRolePermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PermissionsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("Role") + .IsUnique(); + + b.ToTable("TeamRolePermissions"); + }); + + modelBuilder.Entity("Backend.Domain.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SessionToken") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SessionToken") + .IsUnique(); + + b.ToTable("UserSessions"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "SubmittedByTwitchId") + .IsUnique(); + + b.ToTable("VoteBallots"); + + b.HasData( + new + { + Id = 1, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_1" + }, + new + { + Id = 2, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 5, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_2" + }); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BallotId") + .HasColumnType("integer"); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BallotId"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.ToTable("VoteEntries"); + + b.HasData( + new + { + Id = 1, + BallotId = 1, + CandidateId = 1, + CategoryId = 1 + }, + new + { + Id = 2, + BallotId = 1, + CandidateId = 4, + CategoryId = 2 + }, + new + { + Id = 3, + BallotId = 2, + CandidateId = 2, + CategoryId = 1 + }, + new + { + Id = 4, + BallotId = 2, + CandidateId = 6, + CategoryId = 3 + }); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Results") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.HasOne("Backend.Domain.Category", "Category") + .WithMany("Candidates") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Categories") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Candidate"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId"); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.ShowactApplication", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Sponsor", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.HasOne("Backend.Domain.VoteBallot", "Ballot") + .WithMany("Entries") + .HasForeignKey("BallotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ballot"); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Navigation("Candidates"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Navigation("Categories"); + + b.Navigation("Results"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Navigation("Entries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/Migrations/20260628110302_AddSeasonSubcategoryTemplates.cs b/Backend/Migrations/20260628110302_AddSeasonSubcategoryTemplates.cs new file mode 100644 index 0000000..aa4c517 --- /dev/null +++ b/Backend/Migrations/20260628110302_AddSeasonSubcategoryTemplates.cs @@ -0,0 +1,57 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backend.Migrations +{ + /// + public partial class AddSeasonSubcategoryTemplates : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SubcategoryTemplatesJson", + table: "Seasons", + type: "text", + nullable: false, + defaultValue: "[]"); + + migrationBuilder.UpdateData( + table: "Seasons", + keyColumn: "Id", + keyValue: 1, + column: "SubcategoryTemplatesJson", + value: "[]"); + + migrationBuilder.UpdateData( + table: "Seasons", + keyColumn: "Id", + keyValue: 2, + column: "SubcategoryTemplatesJson", + value: "[]"); + + migrationBuilder.UpdateData( + table: "Seasons", + keyColumn: "Id", + keyValue: 3, + column: "SubcategoryTemplatesJson", + value: "[]"); + + migrationBuilder.UpdateData( + table: "Seasons", + keyColumn: "Id", + keyValue: 4, + column: "SubcategoryTemplatesJson", + value: "[]"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SubcategoryTemplatesJson", + table: "Seasons"); + } + } +} diff --git a/Backend/Migrations/20260628115832_AddNominationGroupTrackerIdentity.Designer.cs b/Backend/Migrations/20260628115832_AddNominationGroupTrackerIdentity.Designer.cs new file mode 100644 index 0000000..ff23927 --- /dev/null +++ b/Backend/Migrations/20260628115832_AddNominationGroupTrackerIdentity.Designer.cs @@ -0,0 +1,1936 @@ +// +using System; +using Backend.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backend.Migrations +{ + [DbContext(typeof(AwardsDbContext))] + [Migration("20260628115832_AddNominationGroupTrackerIdentity")] + partial class AddNominationGroupTrackerIdentity + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backend.Domain.AdminAuditEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("AdminTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.ToTable("AdminAuditEntries"); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CategoryName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "CategoryId") + .IsUnique(); + + b.ToTable("Results"); + + b.HasData( + new + { + Id = 1, + CandidateId = 8, + CategoryId = 5, + CategoryName = "VTuber des Jahres", + SeasonId = 2 + }, + new + { + Id = 2, + CandidateId = 9, + CategoryId = 6, + CategoryName = "Bestes Live Event", + SeasonId = 2 + }, + new + { + Id = 3, + CandidateId = 10, + CategoryId = 7, + CategoryName = "Clip des Jahres", + SeasonId = 2 + }, + new + { + Id = 4, + CandidateId = 11, + CategoryId = 8, + CategoryName = "VTuber des Jahres", + SeasonId = 3 + }, + new + { + Id = 5, + CandidateId = 12, + CategoryId = 9, + CategoryName = "Clip des Jahres", + SeasonId = 3 + }, + new + { + Id = 6, + CandidateId = 13, + CategoryId = 10, + CategoryName = "VTuber des Jahres", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AcceptanceNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AcceptanceStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasDefaultValue("open"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ChannelSlug") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ClipCompilationPlatform") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ClipCompilationTitle") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ClipCompilationUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ClipEmbedStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasDefaultValue("unchecked"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("NominationTally") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("StreamerIdentityId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId"); + + b.HasIndex("StreamerIdentityId"); + + b.ToTable("Candidates"); + + b.HasData( + new + { + Id = 1, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@hoshimimiyu", + ClipEmbedStatus = "unchecked", + DisplayName = "Hoshimi Miyu", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 2, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 3, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@shiroch", + ClipEmbedStatus = "unchecked", + DisplayName = "Shiro Ch.", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 4, + AcceptanceStatus = "open", + CategoryId = 2, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu 3D Live", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 5, + AcceptanceStatus = "open", + CategoryId = 2, + ChannelSlug = "@aoisakura", + ClipEmbedStatus = "unchecked", + DisplayName = "Aoi Sakura Showcase", + NominationTally = 0, + Platform = "YouTube", + SeasonId = 1 + }, + new + { + Id = 6, + AcceptanceStatus = "open", + CategoryId = 3, + ChannelSlug = "@pyonkichikingdom", + ClipEmbedStatus = "unchecked", + DisplayName = "Pyonkichi Kingdom", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 7, + AcceptanceStatus = "open", + CategoryId = 4, + ChannelSlug = "@moonrelay", + ClipEmbedStatus = "unchecked", + DisplayName = "Moonrelay", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 8, + AcceptanceStatus = "open", + CategoryId = 5, + ChannelSlug = "@hoshimimiyu", + ClipEmbedStatus = "unchecked", + DisplayName = "Hoshimi Miyu", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 9, + AcceptanceStatus = "open", + CategoryId = 6, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu 3D Live", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 10, + AcceptanceStatus = "open", + CategoryId = 7, + ChannelSlug = "@pyonkichikingdom", + ClipEmbedStatus = "unchecked", + DisplayName = "Pyonkichi Kingdom", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 11, + AcceptanceStatus = "open", + CategoryId = 8, + ChannelSlug = "@aoisakura", + ClipEmbedStatus = "unchecked", + DisplayName = "Aoi Sakura", + NominationTally = 0, + Platform = "YouTube", + SeasonId = 3 + }, + new + { + Id = 12, + AcceptanceStatus = "open", + CategoryId = 9, + ChannelSlug = "@starbyte", + ClipEmbedStatus = "unchecked", + DisplayName = "Starbyte", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 3 + }, + new + { + Id = 13, + AcceptanceStatus = "open", + CategoryId = 10, + ChannelSlug = "@tenshivox", + ClipEmbedStatus = "unchecked", + DisplayName = "Tenshi Vox", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MaxNomineesPerUser") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("ViewerRangeMax") + .HasColumnType("integer"); + + b.Property("ViewerRangeMin") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Slug") + .IsUnique(); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = 1, + Description = "Die groesste Auszeichnung des Jahres.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 1, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 2, + Description = "Events, Konzerte und 3D-Shows.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 1, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 3, + Description = "Der lustigste oder emotionalste Clip des Jahres.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 1, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 4, + Description = "Die aktivste und freundlichste Community.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "Beste Community", + SeasonId = 1, + Slug = "beste-community", + SortOrder = 4 + }, + new + { + Id = 5, + Description = "Archivkategorie 2025.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 2, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 6, + Description = "Archivkategorie 2025.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 2, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 7, + Description = "Archivkategorie 2025.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 2, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 8, + Description = "Archivkategorie 2024.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 3, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 9, + Description = "Archivkategorie 2024.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 3, + Slug = "clip-des-jahres", + SortOrder = 2 + }, + new + { + Id = 10, + Description = "Archivkategorie 2023.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 4, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ClipUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Creator") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("ClipSubmissions"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AvgViewers") + .HasColumnType("integer"); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CandidateText") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CategoryGroupName") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(80) + .HasColumnType("character varying(80)") + .HasDefaultValue(""); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ResolvedChannel") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ResolvedPlatform") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("StreamUrl") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("StreamerIdentityId") + .HasColumnType("integer"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SuggestedCategoryId") + .HasColumnType("integer"); + + b.Property("TrackerCheckedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TrackerStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasDefaultValue("pending"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("StreamerIdentityId"); + + b.HasIndex("SuggestedCategoryId"); + + b.HasIndex("SeasonId", "Status"); + + b.HasIndex("SeasonId", "CategoryGroupName", "Status"); + + b.HasIndex("SeasonId", "StreamerIdentityId", "CategoryGroupName"); + + b.ToTable("Nominations"); + + b.HasData( + new + { + Id = 1, + CandidateText = "Hoshimi Miyu", + CategoryGroupName = "", + CategoryId = 1, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 13, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_hoshi", + TrackerStatus = "pending" + }, + new + { + Id = 2, + CandidateText = "Kurainu 3D Live", + CategoryGroupName = "", + CategoryId = 2, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 14, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_kurainu", + TrackerStatus = "pending" + }); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("TwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("RiskFlags"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrentPhase") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("IsCommunityOnly") + .HasColumnType("boolean"); + + b.Property("IsCurrent") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("NominationEndsAt") + .HasColumnType("date"); + + b.Property("NominationStartsAt") + .HasColumnType("date"); + + b.Property("ReviewEndsAt") + .HasColumnType("date"); + + b.Property("ReviewStartsAt") + .HasColumnType("date"); + + b.Property("ShowDate") + .HasColumnType("date"); + + b.Property("ShowStartsAt") + .HasColumnType("time without time zone"); + + b.Property("ShowStreamUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("SubcategoryTemplatesJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("[]"); + + b.Property("VotingEndsAt") + .HasColumnType("date"); + + b.Property("VotingStartsAt") + .HasColumnType("date"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("Seasons"); + + b.HasData( + new + { + Id = 1, + CurrentPhase = "Community Voting", + IsCommunityOnly = true, + IsCurrent = true, + Name = "VTuber Star Awards 2026", + NominationEndsAt = new DateOnly(2026, 5, 31), + NominationStartsAt = new DateOnly(2026, 5, 1), + ReviewEndsAt = new DateOnly(2026, 7, 10), + ReviewStartsAt = new DateOnly(2026, 7, 1), + ShowDate = new DateOnly(2026, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + SubcategoryTemplatesJson = "[]", + VotingEndsAt = new DateOnly(2026, 6, 30), + VotingStartsAt = new DateOnly(2026, 6, 1), + Year = 2026 + }, + new + { + Id = 2, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2025", + NominationEndsAt = new DateOnly(2025, 5, 31), + NominationStartsAt = new DateOnly(2025, 5, 1), + ReviewEndsAt = new DateOnly(2025, 7, 10), + ReviewStartsAt = new DateOnly(2025, 7, 1), + ShowDate = new DateOnly(2025, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + SubcategoryTemplatesJson = "[]", + VotingEndsAt = new DateOnly(2025, 6, 30), + VotingStartsAt = new DateOnly(2025, 6, 1), + Year = 2025 + }, + new + { + Id = 3, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2024", + NominationEndsAt = new DateOnly(2024, 5, 31), + NominationStartsAt = new DateOnly(2024, 5, 1), + ReviewEndsAt = new DateOnly(2024, 7, 10), + ReviewStartsAt = new DateOnly(2024, 7, 1), + ShowDate = new DateOnly(2024, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://youtube.com/c/Jayuhime", + SubcategoryTemplatesJson = "[]", + VotingEndsAt = new DateOnly(2024, 6, 30), + VotingStartsAt = new DateOnly(2024, 6, 1), + Year = 2024 + }, + new + { + Id = 4, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2023", + NominationEndsAt = new DateOnly(2023, 5, 31), + NominationStartsAt = new DateOnly(2023, 5, 1), + ReviewEndsAt = new DateOnly(2023, 7, 10), + ReviewStartsAt = new DateOnly(2023, 7, 1), + ShowDate = new DateOnly(2023, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + SubcategoryTemplatesJson = "[]", + VotingEndsAt = new DateOnly(2023, 6, 30), + VotingStartsAt = new DateOnly(2023, 6, 1), + Year = 2023 + }); + }); + + modelBuilder.Entity("Backend.Domain.ShowactApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ArtistName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ContactDiscord") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ContactEmail") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("FieldResponsesJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("PerformanceType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("PlatformUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReferenceUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TechnicalNotes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("ShowactApplications"); + }); + + modelBuilder.Entity("Backend.Domain.SiteSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClipAdminMenuVisible") + .HasColumnType("boolean"); + + b.Property("ClipReviewEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ClipSubmissionDisabledMessage") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("ClipSubmissionsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ContactContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("DemoLoginDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginEmail") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("DemoLoginEnabled") + .HasColumnType("boolean"); + + b.Property("DemoLoginManagedByDatabase") + .HasColumnType("boolean"); + + b.Property("DemoLoginPasswordHash") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginPasswordSalt") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DemoLoginTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("FaqJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("HostDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("HostTagline") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("ImprintContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("ImprintUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("MaintenanceMessage") + .IsRequired() + .HasMaxLength(600) + .HasColumnType("character varying(600)"); + + b.Property("MaintenanceModeEnabled") + .HasColumnType("boolean"); + + b.Property("MaintenanceTitle") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("NewsletterUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("NominationLinkBlacklistJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("[]"); + + b.Property("PrivacyEmail") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("PrivacyPolicyContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("PrivacyPolicyUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivacyPolicyUpdatedBy") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("RiskRulesJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SessionIdleTimeoutHours") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(3); + + b.Property("ShareDiscordUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShareXUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShowactApplicationDisabledMessage") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("ShowactApplicationsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ShowactFormSchemaJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShowactsContent") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue(""); + + b.Property("ShowactsUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("SocialLinksJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("SponsorsVisible") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("TwitchAuthManagedByDatabase") + .HasColumnType("boolean"); + + b.Property("TwitchClientId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchClientSecret") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("TwitchRedirectUri") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("TwitchScope") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("WorkflowRulesJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("[]"); + + b.HasKey("Id"); + + b.ToTable("SiteSettings"); + + b.HasData( + new + { + Id = 1, + ClipAdminMenuVisible = true, + ClipReviewEnabled = true, + ClipSubmissionDisabledMessage = "Clip-Einreichungen sind aktuell geschlossen.", + ClipSubmissionsEnabled = false, + ContactContent = "Kontakt zum Award-Team\nDu hast Fragen zur Nominierung, zum Voting, zu Clips oder zur Show? Schreib dem Team ueber die hinterlegte Kontaktseite.\n\nDatenschutzfragen\nFuer Datenschutzanfragen nutze bitte die Datenschutz-E-Mail aus diesem Footer.\n\nCommunity & Kooperationen\nSocial Links und Partneranfragen werden vom Admin-Team gepflegt und koordiniert.", + ContactUrl = "https://vtuber-star-awards.de/kontakt", + DemoLoginDisplayName = "Jayuhime Admin", + DemoLoginEmail = "", + DemoLoginEnabled = false, + DemoLoginManagedByDatabase = false, + DemoLoginPasswordHash = "", + DemoLoginPasswordSalt = "", + DemoLoginTwitchUserId = "jayuhime_admin", + FaqJson = "[{\"question\":\"Wer darf nominiert werden?\",\"answer\":\"Jede:r aktive deutschsprachige VTuber kann nominiert werden \\u2014 unabh\\u00E4ngig von Follower-Zahl oder Plattform. Die Community schl\\u00E4gt in der Nominierungsphase ihre Favorit:innen vor.\"},{\"question\":\"Wie funktioniert das Voting?\",\"answer\":\"Du meldest dich ausschlie\\u00DFlich mit deinem Twitch-Account an \\u2014 nur so kannst du teilnehmen. Das h\\u00E4lt das Voting fair und bot-frei. Danach vergibst du pro Kategorie eine Stimme, \\u00E4nderbar bis zum Ende der Phase.\"},{\"question\":\"Was kostet die Teilnahme?\",\"answer\":\"Gar nichts. Nominieren, voten und die Show schauen ist komplett kostenlos \\u2014 der VTuber Star Award ist ein Community-Event von Fans f\\u00FCr Fans.\"},{\"question\":\"Wann und wo findet die Award-Show statt?\",\"answer\":\"Die gro\\u00DFe Live-Show wird von Jayuhime gehostet und auf Twitch \\u0026 YouTube gestreamt. Den genauen Termin findest du im Countdown oben \\u2014 sei live dabei, wenn die Stars gek\\u00FCrt werden!\"},{\"question\":\"Ich wurde nominiert \\u2014 was nun?\",\"answer\":\"Gl\\u00FCckwunsch! Du erh\\u00E4ltst eine Benachrichtigung mit allen Infos. Teile deine Nominierung gern mit deiner Community und ruf zum Voten auf \\u2014 jede Stimme z\\u00E4hlt.\"}]", + HostDisplayName = "Jayuhime", + HostTagline = "VTuber & Award Host", + ImprintContent = "Anbieter\nVTuber Star Awards, vertreten durch Jayuhime.\n\nKontakt\nNutze fuer organisatorische Fragen bitte die Kontaktseite oder die hinterlegte Kontaktadresse.\n\nHinweis\nDieses Impressum ist ein redaktioneller Platzhalter und sollte vor dem Livegang mit den tatsaechlichen Anbieterangaben ersetzt werden.", + ImprintUrl = "https://vtuber-star-awards.de/impressum", + MaintenanceMessage = "Die Award-Galaxie wird gerade liebevoll poliert. Schau gleich wieder vorbei.", + MaintenanceModeEnabled = false, + MaintenanceTitle = "Sternenpause", + NewsletterUrl = "https://vtuber-star-awards.de/newsletter", + NominationLinkBlacklistJson = "[{\"Url\":\"https://kick.com/\"},{\"Url\":\"https://www.twitch.tv/\"},{\"Url\":\"https://www.youtube.com/\"}]", + PrivacyEmail = "datenschutz@vtuber-star-awards.de", + PrivacyPolicyContent = "Verantwortliche:r\nVTuber Star Awards, vertreten durch Jayuhime. Kontakt: datenschutz@vtuber-star-awards.de\n\nWelche Daten wir verarbeiten\nBei Teilnahme (Voting, Nominierung, Clip-Einreichung) verarbeiten wir ausschließlich deine Twitch-User-ID sowie den Zeitstempel deiner Aktion.\nFür Show-Erinnerungen speichern wir optional deine E-Mail-Adresse.\n\nRechtsgrundlage\nVerarbeitung auf Basis von Art. 6 Abs. 1 lit. b DSGVO. Für optionale Erinnerungen gilt Art. 6 Abs. 1 lit. a DSGVO.\n\nZweck der Verarbeitung\nDurchführung des VTuber Star Awards, Sicherstellung fairer Abstimmung, Spam-Prävention sowie Admin-Review von Einreichungen.\n\nLöschfristen\nAlle Teilnahmedaten werden spätestens 6 Monate nach der Award-Show automatisch gelöscht. E-Mail-Adressen werden nach der Show entfernt.\n\nDeine Rechte\nDu hast das Recht auf Auskunft, Löschung, Berichtigung, Widerspruch und Beschwerde bei der zuständigen Aufsichtsbehörde.\n\nWeitergabe an Dritte\nKeine Weitergabe zu Werbezwecken. Technische Dienstleister verarbeiten Daten ausschließlich im Rahmen der Auftragsverarbeitung.", + PrivacyPolicyUpdatedAt = new DateTimeOffset(new DateTime(2026, 6, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + PrivacyPolicyUpdatedBy = "seed", + RiskRulesJson = "[{\"key\":\"resubmitted_ballot\",\"label\":\"Ballot erneut gespeichert\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"low\",\"description\":\"Erstellt einen Hinweis, wenn ein User sein Voting erneut speichert.\"},{\"key\":\"rapid_vote_updates\",\"label\":\"Voting-Burst\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr schnell mehrere Voting-Aenderungen ausloest.\"},{\"key\":\"resubmitted_nomination\",\"label\":\"Nominierung erneut eingereicht\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"low\",\"description\":\"Erstellt einen Hinweis, wenn ein User in derselben Kategorie erneut nominiert.\"},{\"key\":\"rapid_nomination_burst\",\"label\":\"Nominierungs-Burst\",\"enabled\":true,\"threshold\":10,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr viele Nominierungen in kurzer Zeit sendet.\"},{\"key\":\"duplicate_clip_submission\",\"label\":\"Doppelter Clip\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn ein User denselben Clip erneut einreicht.\"},{\"key\":\"rapid_clip_burst\",\"label\":\"Clip-Burst\",\"enabled\":true,\"threshold\":5,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr viele Clips in kurzer Zeit sendet.\"},{\"key\":\"rapid_login_ip\",\"label\":\"Login-Burst pro IP\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":15,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn mehrere neue Sessions von derselben IP entstehen.\"},{\"key\":\"rapid_demo_login_ip\",\"label\":\"Demo-Login-Burst pro IP\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":15,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn mehrere Demo-Admin-Sessions von derselben IP entstehen.\"}]", + SessionIdleTimeoutHours = 3, + ShareDiscordUrl = "", + ShareXUrl = "", + ShowactApplicationDisabledMessage = "Showact-Bewerbungen sind aktuell geschlossen.", + ShowactApplicationsEnabled = false, + ShowactFormSchemaJson = "[]", + ShowactsContent = "Informationen zu Showact-Bewerbungen, Ablauf und Kontakt fuer die Award-Show.", + ShowactsUrl = "https://vtuber-star-awards.de/showacts", + SocialLinksJson = "[{\"label\":\"Twitch\",\"platform\":\"twitch\",\"url\":\"https://twitch.tv/jayuhime\",\"icon\":\"twitch\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"YouTube\",\"platform\":\"youtube\",\"url\":\"https://youtube.com/c/Jayuhime\",\"icon\":\"youtube\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"X\",\"platform\":\"x\",\"url\":\"https://x.com/jayuhime\",\"icon\":\"x\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Instagram\",\"platform\":\"instagram\",\"url\":\"https://instagram.com/jayuhime\",\"icon\":\"instagram\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Discord\",\"platform\":\"discord\",\"url\":\"https://discord.gg/jayuhime\",\"icon\":\"discord\",\"showOnHost\":true,\"showOnCommunity\":true}]", + SponsorsContent = "Sponsoren & Partner\nHier koennen Sponsor:innen, Medienpartner, Community-Partner und Supporter des VTuber Star Awards vorgestellt werden.\n\nPartner werden im Rahmen der Show und auf den oeffentlichen Kontaktflaechen genannt, sobald sie final bestaetigt sind.", + SponsorsUrl = "https://vtuber-star-awards.de/partner", + SponsorsVisible = true, + TwitchAuthManagedByDatabase = false, + TwitchClientId = "", + TwitchClientSecret = "", + TwitchRedirectUri = "", + TwitchScope = "", + WorkflowRulesJson = "[{\"key\":\"max_finalists_per_category\",\"label\":\"Finale Kandidat:innen pro Kategorie\",\"enabled\":true,\"limit\":4,\"mode\":\"block\",\"description\":\"Begrenzt, wie viele finale Kandidat:innen in einer Kategorie vorbereitet werden.\"},{\"key\":\"max_candidate_appearances\",\"label\":\"Kandidaturen pro Person\",\"enabled\":true,\"limit\":2,\"mode\":\"warn\",\"description\":\"Warnt oder blockiert, wenn dieselbe Person in zu vielen Kategorien final auftaucht.\"},{\"key\":\"max_winner_placements\",\"label\":\"Gewinnerpl\\u00E4tze pro Person\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn dieselbe Person mehrfach als Gewinner:in gesetzt wird.\"},{\"key\":\"winner_requires_clip\",\"label\":\"Gewinner braucht Clip-Link\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn ein Gewinner ohne gepflegten YouTube-/Twitch-Clip gesetzt wird.\"}]" + }); + }); + + modelBuilder.Entity("Backend.Domain.Sponsor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsVisible") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tier") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WebsiteUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "IsVisible", "SortOrder"); + + b.ToTable("Sponsors"); + }); + + modelBuilder.Entity("Backend.Domain.StreamerIdentity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("LastResolvedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Login") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("NormalizedKey") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ProfileUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedKey") + .IsUnique(); + + b.ToTable("StreamerIdentities"); + }); + + modelBuilder.Entity("Backend.Domain.TeamMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoundTwitchDisplayName") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("BoundTwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Login") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MustChangePassword") + .HasColumnType("boolean"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("PasswordResetAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("TwitchBoundAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("BoundTwitchUserId") + .IsUnique(); + + b.HasIndex("Login") + .IsUnique(); + + b.ToTable("TeamMembers"); + }); + + modelBuilder.Entity("Backend.Domain.TeamRolePermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PermissionsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("Role") + .IsUnique(); + + b.ToTable("TeamRolePermissions"); + }); + + modelBuilder.Entity("Backend.Domain.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SessionToken") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SessionToken") + .IsUnique(); + + b.ToTable("UserSessions"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "SubmittedByTwitchId") + .IsUnique(); + + b.ToTable("VoteBallots"); + + b.HasData( + new + { + Id = 1, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_1" + }, + new + { + Id = 2, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 5, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_2" + }); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BallotId") + .HasColumnType("integer"); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BallotId"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.ToTable("VoteEntries"); + + b.HasData( + new + { + Id = 1, + BallotId = 1, + CandidateId = 1, + CategoryId = 1 + }, + new + { + Id = 2, + BallotId = 1, + CandidateId = 4, + CategoryId = 2 + }, + new + { + Id = 3, + BallotId = 2, + CandidateId = 2, + CategoryId = 1 + }, + new + { + Id = 4, + BallotId = 2, + CandidateId = 6, + CategoryId = 3 + }); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Results") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.HasOne("Backend.Domain.Category", "Category") + .WithMany("Candidates") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.StreamerIdentity", "StreamerIdentity") + .WithMany("Candidates") + .HasForeignKey("StreamerIdentityId"); + + b.Navigation("Category"); + + b.Navigation("Season"); + + b.Navigation("StreamerIdentity"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Categories") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Candidate"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId"); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.StreamerIdentity", "StreamerIdentity") + .WithMany("Nominations") + .HasForeignKey("StreamerIdentityId"); + + b.HasOne("Backend.Domain.Category", "SuggestedCategory") + .WithMany() + .HasForeignKey("SuggestedCategoryId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + + b.Navigation("StreamerIdentity"); + + b.Navigation("SuggestedCategory"); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.ShowactApplication", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Sponsor", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.HasOne("Backend.Domain.VoteBallot", "Ballot") + .WithMany("Entries") + .HasForeignKey("BallotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ballot"); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Navigation("Candidates"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Navigation("Categories"); + + b.Navigation("Results"); + }); + + modelBuilder.Entity("Backend.Domain.StreamerIdentity", b => + { + b.Navigation("Candidates"); + + b.Navigation("Nominations"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Navigation("Entries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/Migrations/20260628115832_AddNominationGroupTrackerIdentity.cs b/Backend/Migrations/20260628115832_AddNominationGroupTrackerIdentity.cs new file mode 100644 index 0000000..58eec8c --- /dev/null +++ b/Backend/Migrations/20260628115832_AddNominationGroupTrackerIdentity.cs @@ -0,0 +1,388 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backend.Migrations +{ + /// + public partial class AddNominationGroupTrackerIdentity : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Nominations_Categories_CategoryId", + table: "Nominations"); + + migrationBuilder.AlterColumn( + name: "CategoryId", + table: "Nominations", + type: "integer", + nullable: true, + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AddColumn( + name: "AvgViewers", + table: "Nominations", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "CategoryGroupName", + table: "Nominations", + type: "character varying(80)", + maxLength: 80, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "ResolvedChannel", + table: "Nominations", + type: "character varying(120)", + maxLength: 120, + nullable: true); + + migrationBuilder.AddColumn( + name: "ResolvedPlatform", + table: "Nominations", + type: "character varying(40)", + maxLength: 40, + nullable: true); + + migrationBuilder.AddColumn( + name: "StreamerIdentityId", + table: "Nominations", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "SuggestedCategoryId", + table: "Nominations", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "TrackerCheckedAt", + table: "Nominations", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "TrackerStatus", + table: "Nominations", + type: "character varying(40)", + maxLength: 40, + nullable: false, + defaultValue: "pending"); + + migrationBuilder.AddColumn( + name: "NominationTally", + table: "Candidates", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "StreamerIdentityId", + table: "Candidates", + type: "integer", + nullable: true); + + migrationBuilder.CreateTable( + name: "StreamerIdentities", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Platform = table.Column(type: "character varying(40)", maxLength: 40, nullable: false), + Login = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), + NormalizedKey = table.Column(type: "character varying(180)", maxLength: 180, nullable: false), + DisplayName = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), + ProfileUrl = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), + LastResolvedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_StreamerIdentities", x => x.Id); + }); + + migrationBuilder.UpdateData( + table: "Candidates", + keyColumn: "Id", + keyValue: 1, + column: "StreamerIdentityId", + value: null); + + migrationBuilder.UpdateData( + table: "Candidates", + keyColumn: "Id", + keyValue: 2, + column: "StreamerIdentityId", + value: null); + + migrationBuilder.UpdateData( + table: "Candidates", + keyColumn: "Id", + keyValue: 3, + column: "StreamerIdentityId", + value: null); + + migrationBuilder.UpdateData( + table: "Candidates", + keyColumn: "Id", + keyValue: 4, + column: "StreamerIdentityId", + value: null); + + migrationBuilder.UpdateData( + table: "Candidates", + keyColumn: "Id", + keyValue: 5, + column: "StreamerIdentityId", + value: null); + + migrationBuilder.UpdateData( + table: "Candidates", + keyColumn: "Id", + keyValue: 6, + column: "StreamerIdentityId", + value: null); + + migrationBuilder.UpdateData( + table: "Candidates", + keyColumn: "Id", + keyValue: 7, + column: "StreamerIdentityId", + value: null); + + migrationBuilder.UpdateData( + table: "Candidates", + keyColumn: "Id", + keyValue: 8, + column: "StreamerIdentityId", + value: null); + + migrationBuilder.UpdateData( + table: "Candidates", + keyColumn: "Id", + keyValue: 9, + column: "StreamerIdentityId", + value: null); + + migrationBuilder.UpdateData( + table: "Candidates", + keyColumn: "Id", + keyValue: 10, + column: "StreamerIdentityId", + value: null); + + migrationBuilder.UpdateData( + table: "Candidates", + keyColumn: "Id", + keyValue: 11, + column: "StreamerIdentityId", + value: null); + + migrationBuilder.UpdateData( + table: "Candidates", + keyColumn: "Id", + keyValue: 12, + column: "StreamerIdentityId", + value: null); + + migrationBuilder.UpdateData( + table: "Candidates", + keyColumn: "Id", + keyValue: 13, + column: "StreamerIdentityId", + value: null); + + migrationBuilder.UpdateData( + table: "Nominations", + keyColumn: "Id", + keyValue: 1, + columns: new[] { "AvgViewers", "CategoryGroupName", "ResolvedChannel", "ResolvedPlatform", "StreamerIdentityId", "SuggestedCategoryId", "TrackerCheckedAt", "TrackerStatus" }, + values: new object[] { null, "", null, null, null, null, null, "pending" }); + + migrationBuilder.UpdateData( + table: "Nominations", + keyColumn: "Id", + keyValue: 2, + columns: new[] { "AvgViewers", "CategoryGroupName", "ResolvedChannel", "ResolvedPlatform", "StreamerIdentityId", "SuggestedCategoryId", "TrackerCheckedAt", "TrackerStatus" }, + values: new object[] { null, "", null, null, null, null, null, "pending" }); + + migrationBuilder.Sql(""" + UPDATE "Nominations" n + SET "CategoryGroupName" = c."GroupName" + FROM "Categories" c + WHERE n."CategoryId" = c."Id" + AND COALESCE(n."CategoryGroupName", '') = ''; + """); + + migrationBuilder.CreateIndex( + name: "IX_Nominations_SeasonId_CategoryGroupName_Status", + table: "Nominations", + columns: new[] { "SeasonId", "CategoryGroupName", "Status" }); + + migrationBuilder.CreateIndex( + name: "IX_Nominations_SeasonId_StreamerIdentityId_CategoryGroupName", + table: "Nominations", + columns: new[] { "SeasonId", "StreamerIdentityId", "CategoryGroupName" }); + + migrationBuilder.CreateIndex( + name: "IX_Nominations_StreamerIdentityId", + table: "Nominations", + column: "StreamerIdentityId"); + + migrationBuilder.CreateIndex( + name: "IX_Nominations_SuggestedCategoryId", + table: "Nominations", + column: "SuggestedCategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_Candidates_StreamerIdentityId", + table: "Candidates", + column: "StreamerIdentityId"); + + migrationBuilder.CreateIndex( + name: "IX_StreamerIdentities_NormalizedKey", + table: "StreamerIdentities", + column: "NormalizedKey", + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_Candidates_StreamerIdentities_StreamerIdentityId", + table: "Candidates", + column: "StreamerIdentityId", + principalTable: "StreamerIdentities", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Nominations_Categories_CategoryId", + table: "Nominations", + column: "CategoryId", + principalTable: "Categories", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + + migrationBuilder.AddForeignKey( + name: "FK_Nominations_Categories_SuggestedCategoryId", + table: "Nominations", + column: "SuggestedCategoryId", + principalTable: "Categories", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + + migrationBuilder.AddForeignKey( + name: "FK_Nominations_StreamerIdentities_StreamerIdentityId", + table: "Nominations", + column: "StreamerIdentityId", + principalTable: "StreamerIdentities", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Candidates_StreamerIdentities_StreamerIdentityId", + table: "Candidates"); + + migrationBuilder.DropForeignKey( + name: "FK_Nominations_Categories_CategoryId", + table: "Nominations"); + + migrationBuilder.DropForeignKey( + name: "FK_Nominations_Categories_SuggestedCategoryId", + table: "Nominations"); + + migrationBuilder.DropForeignKey( + name: "FK_Nominations_StreamerIdentities_StreamerIdentityId", + table: "Nominations"); + + migrationBuilder.DropTable( + name: "StreamerIdentities"); + + migrationBuilder.DropIndex( + name: "IX_Nominations_SeasonId_CategoryGroupName_Status", + table: "Nominations"); + + migrationBuilder.DropIndex( + name: "IX_Nominations_SeasonId_StreamerIdentityId_CategoryGroupName", + table: "Nominations"); + + migrationBuilder.DropIndex( + name: "IX_Nominations_StreamerIdentityId", + table: "Nominations"); + + migrationBuilder.DropIndex( + name: "IX_Nominations_SuggestedCategoryId", + table: "Nominations"); + + migrationBuilder.DropIndex( + name: "IX_Candidates_StreamerIdentityId", + table: "Candidates"); + + migrationBuilder.DropColumn( + name: "AvgViewers", + table: "Nominations"); + + migrationBuilder.DropColumn( + name: "CategoryGroupName", + table: "Nominations"); + + migrationBuilder.DropColumn( + name: "ResolvedChannel", + table: "Nominations"); + + migrationBuilder.DropColumn( + name: "ResolvedPlatform", + table: "Nominations"); + + migrationBuilder.DropColumn( + name: "StreamerIdentityId", + table: "Nominations"); + + migrationBuilder.DropColumn( + name: "SuggestedCategoryId", + table: "Nominations"); + + migrationBuilder.DropColumn( + name: "TrackerCheckedAt", + table: "Nominations"); + + migrationBuilder.DropColumn( + name: "TrackerStatus", + table: "Nominations"); + + migrationBuilder.DropColumn( + name: "NominationTally", + table: "Candidates"); + + migrationBuilder.DropColumn( + name: "StreamerIdentityId", + table: "Candidates"); + + migrationBuilder.AlterColumn( + name: "CategoryId", + table: "Nominations", + type: "integer", + nullable: false, + defaultValue: 0, + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true); + + migrationBuilder.AddForeignKey( + name: "FK_Nominations_Categories_CategoryId", + table: "Nominations", + column: "CategoryId", + principalTable: "Categories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/Backend/Migrations/20260629142009_InitialCleanBaseline.Designer.cs b/Backend/Migrations/20260628161949_AddShowactApplicationSchedule.Designer.cs similarity index 62% rename from Backend/Migrations/20260629142009_InitialCleanBaseline.Designer.cs rename to Backend/Migrations/20260628161949_AddShowactApplicationSchedule.Designer.cs index 65ca5ae..1165a2b 100644 --- a/Backend/Migrations/20260629142009_InitialCleanBaseline.Designer.cs +++ b/Backend/Migrations/20260628161949_AddShowactApplicationSchedule.Designer.cs @@ -12,8 +12,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Backend.Migrations { [DbContext(typeof(AwardsDbContext))] - [Migration("20260629142009_InitialCleanBaseline")] - partial class InitialCleanBaseline + [Migration("20260628161949_AddShowactApplicationSchedule")] + partial class AddShowactApplicationSchedule { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -112,6 +112,56 @@ namespace Backend.Migrations .IsUnique(); b.ToTable("Results"); + + b.HasData( + new + { + Id = 1, + CandidateId = 8, + CategoryId = 5, + CategoryName = "VTuber des Jahres", + SeasonId = 2 + }, + new + { + Id = 2, + CandidateId = 9, + CategoryId = 6, + CategoryName = "Bestes Live Event", + SeasonId = 2 + }, + new + { + Id = 3, + CandidateId = 10, + CategoryId = 7, + CategoryName = "Clip des Jahres", + SeasonId = 2 + }, + new + { + Id = 4, + CandidateId = 11, + CategoryId = 8, + CategoryName = "VTuber des Jahres", + SeasonId = 3 + }, + new + { + Id = 5, + CandidateId = 12, + CategoryId = 9, + CategoryName = "Clip des Jahres", + SeasonId = 3 + }, + new + { + Id = 6, + CandidateId = 13, + CategoryId = 10, + CategoryName = "VTuber des Jahres", + SeasonId = 4 + }); }); modelBuilder.Entity("Backend.Domain.Candidate", b => @@ -190,6 +240,164 @@ namespace Backend.Migrations b.HasIndex("StreamerIdentityId"); b.ToTable("Candidates"); + + b.HasData( + new + { + Id = 1, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@hoshimimiyu", + ClipEmbedStatus = "unchecked", + DisplayName = "Hoshimi Miyu", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 2, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 3, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@shiroch", + ClipEmbedStatus = "unchecked", + DisplayName = "Shiro Ch.", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 4, + AcceptanceStatus = "open", + CategoryId = 2, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu 3D Live", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 5, + AcceptanceStatus = "open", + CategoryId = 2, + ChannelSlug = "@aoisakura", + ClipEmbedStatus = "unchecked", + DisplayName = "Aoi Sakura Showcase", + NominationTally = 0, + Platform = "YouTube", + SeasonId = 1 + }, + new + { + Id = 6, + AcceptanceStatus = "open", + CategoryId = 3, + ChannelSlug = "@pyonkichikingdom", + ClipEmbedStatus = "unchecked", + DisplayName = "Pyonkichi Kingdom", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 7, + AcceptanceStatus = "open", + CategoryId = 4, + ChannelSlug = "@moonrelay", + ClipEmbedStatus = "unchecked", + DisplayName = "Moonrelay", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 8, + AcceptanceStatus = "open", + CategoryId = 5, + ChannelSlug = "@hoshimimiyu", + ClipEmbedStatus = "unchecked", + DisplayName = "Hoshimi Miyu", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 9, + AcceptanceStatus = "open", + CategoryId = 6, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu 3D Live", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 10, + AcceptanceStatus = "open", + CategoryId = 7, + ChannelSlug = "@pyonkichikingdom", + ClipEmbedStatus = "unchecked", + DisplayName = "Pyonkichi Kingdom", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 11, + AcceptanceStatus = "open", + CategoryId = 8, + ChannelSlug = "@aoisakura", + ClipEmbedStatus = "unchecked", + DisplayName = "Aoi Sakura", + NominationTally = 0, + Platform = "YouTube", + SeasonId = 3 + }, + new + { + Id = 12, + AcceptanceStatus = "open", + CategoryId = 9, + ChannelSlug = "@starbyte", + ClipEmbedStatus = "unchecked", + DisplayName = "Starbyte", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 3 + }, + new + { + Id = 13, + AcceptanceStatus = "open", + CategoryId = 10, + ChannelSlug = "@tenshivox", + ClipEmbedStatus = "unchecked", + DisplayName = "Tenshi Vox", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 4 + }); }); modelBuilder.Entity("Backend.Domain.Category", b => @@ -240,6 +448,118 @@ namespace Backend.Migrations .IsUnique(); b.ToTable("Categories"); + + b.HasData( + new + { + Id = 1, + Description = "Die groesste Auszeichnung des Jahres.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 1, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 2, + Description = "Events, Konzerte und 3D-Shows.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 1, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 3, + Description = "Der lustigste oder emotionalste Clip des Jahres.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 1, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 4, + Description = "Die aktivste und freundlichste Community.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "Beste Community", + SeasonId = 1, + Slug = "beste-community", + SortOrder = 4 + }, + new + { + Id = 5, + Description = "Archivkategorie 2025.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 2, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 6, + Description = "Archivkategorie 2025.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 2, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 7, + Description = "Archivkategorie 2025.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 2, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 8, + Description = "Archivkategorie 2024.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 3, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 9, + Description = "Archivkategorie 2024.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 3, + Slug = "clip-des-jahres", + SortOrder = 2 + }, + new + { + Id = 10, + Description = "Archivkategorie 2023.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 4, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }); }); modelBuilder.Entity("Backend.Domain.ClipSubmission", b => @@ -348,18 +668,6 @@ namespace Backend.Migrations b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); - b.Property("FollowersGained") - .HasColumnType("integer"); - - b.Property("HoursStreamed") - .HasColumnType("integer"); - - b.Property("HoursWatched") - .HasColumnType("integer"); - - b.Property("PeakViewers") - .HasColumnType("integer"); - b.Property("ResolvedChannel") .HasMaxLength(120) .HasColumnType("character varying(120)"); @@ -412,30 +720,6 @@ namespace Backend.Migrations .HasColumnType("character varying(40)") .HasDefaultValue("pending"); - b.Property("TrackingFlagsJson") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("text") - .HasDefaultValue("[]"); - - b.Property("TrackingReviewNote") - .HasMaxLength(1000) - .HasColumnType("character varying(1000)"); - - b.Property("TrackingReviewStatus") - .IsRequired() - .ValueGeneratedOnAdd() - .HasMaxLength(30) - .HasColumnType("character varying(30)") - .HasDefaultValue("clear"); - - b.Property("TrackingReviewedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("TrackingReviewedByTwitchId") - .HasMaxLength(120) - .HasColumnType("character varying(120)"); - b.HasKey("Id"); b.HasIndex("CandidateId"); @@ -453,6 +737,32 @@ namespace Backend.Migrations b.HasIndex("SeasonId", "StreamerIdentityId", "CategoryGroupName"); b.ToTable("Nominations"); + + b.HasData( + new + { + Id = 1, + CandidateText = "Hoshimi Miyu", + CategoryGroupName = "", + CategoryId = 1, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 13, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_hoshi", + TrackerStatus = "pending" + }, + new + { + Id = 2, + CandidateText = "Kurainu 3D Live", + CategoryGroupName = "", + CategoryId = 2, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 14, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_kurainu", + TrackerStatus = "pending" + }); }); modelBuilder.Entity("Backend.Domain.RiskFlag", b => @@ -549,11 +859,6 @@ namespace Backend.Migrations b.Property("IsCurrent") .HasColumnType("boolean"); - b.Property("IsDemo") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false); - b.Property("Name") .IsRequired() .HasMaxLength(160) @@ -577,6 +882,11 @@ namespace Backend.Migrations b.Property("ShowStartsAt") .HasColumnType("time without time zone"); + b.Property("ShowStreamUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + b.Property("SubcategoryTemplatesJson") .IsRequired() .ValueGeneratedOnAdd() @@ -589,19 +899,6 @@ namespace Backend.Migrations b.Property("VotingStartsAt") .HasColumnType("date"); - b.Property("WinnersPublishedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("WinnersPublishedByTwitchId") - .HasMaxLength(120) - .HasColumnType("character varying(120)"); - - b.Property("WorkflowRulesJson") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("text") - .HasDefaultValue("[]"); - b.Property("Year") .HasColumnType("integer"); @@ -611,6 +908,84 @@ namespace Backend.Migrations .IsUnique(); b.ToTable("Seasons"); + + b.HasData( + new + { + Id = 1, + CurrentPhase = "Community Voting", + IsCommunityOnly = true, + IsCurrent = true, + Name = "VTuber Star Awards 2026", + NominationEndsAt = new DateOnly(2026, 5, 31), + NominationStartsAt = new DateOnly(2026, 5, 1), + ReviewEndsAt = new DateOnly(2026, 7, 10), + ReviewStartsAt = new DateOnly(2026, 7, 1), + ShowDate = new DateOnly(2026, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + SubcategoryTemplatesJson = "[]", + VotingEndsAt = new DateOnly(2026, 6, 30), + VotingStartsAt = new DateOnly(2026, 6, 1), + Year = 2026 + }, + new + { + Id = 2, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2025", + NominationEndsAt = new DateOnly(2025, 5, 31), + NominationStartsAt = new DateOnly(2025, 5, 1), + ReviewEndsAt = new DateOnly(2025, 7, 10), + ReviewStartsAt = new DateOnly(2025, 7, 1), + ShowDate = new DateOnly(2025, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + SubcategoryTemplatesJson = "[]", + VotingEndsAt = new DateOnly(2025, 6, 30), + VotingStartsAt = new DateOnly(2025, 6, 1), + Year = 2025 + }, + new + { + Id = 3, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2024", + NominationEndsAt = new DateOnly(2024, 5, 31), + NominationStartsAt = new DateOnly(2024, 5, 1), + ReviewEndsAt = new DateOnly(2024, 7, 10), + ReviewStartsAt = new DateOnly(2024, 7, 1), + ShowDate = new DateOnly(2024, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://youtube.com/c/Jayuhime", + SubcategoryTemplatesJson = "[]", + VotingEndsAt = new DateOnly(2024, 6, 30), + VotingStartsAt = new DateOnly(2024, 6, 1), + Year = 2024 + }, + new + { + Id = 4, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2023", + NominationEndsAt = new DateOnly(2023, 5, 31), + NominationStartsAt = new DateOnly(2023, 5, 1), + ReviewEndsAt = new DateOnly(2023, 7, 10), + ReviewStartsAt = new DateOnly(2023, 7, 1), + ShowDate = new DateOnly(2023, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + SubcategoryTemplatesJson = "[]", + VotingEndsAt = new DateOnly(2023, 6, 30), + VotingStartsAt = new DateOnly(2023, 6, 1), + Year = 2023 + }); }); modelBuilder.Entity("Backend.Domain.ShowactApplication", b => @@ -712,14 +1087,6 @@ namespace Backend.Migrations NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - b.Property("AwardsSectionDescription") - .IsRequired() - .HasColumnType("text"); - - b.Property("AwardsSectionTitle") - .IsRequired() - .HasColumnType("text"); - b.Property("ClipAdminMenuVisible") .HasColumnType("boolean"); @@ -907,80 +1274,6 @@ namespace Backend.Migrations .HasColumnType("boolean") .HasDefaultValue(true); - b.Property("StreamBannerCompletedButtonLabel") - .IsRequired() - .HasMaxLength(120) - .HasColumnType("character varying(120)"); - - b.Property("StreamBannerCompletedButtonUrl") - .IsRequired() - .HasMaxLength(400) - .HasColumnType("character varying(400)"); - - b.Property("StreamBannerCompletedEyebrow") - .IsRequired() - .HasMaxLength(120) - .HasColumnType("character varying(120)"); - - b.Property("StreamBannerCompletedText") - .IsRequired() - .HasColumnType("text"); - - b.Property("StreamBannerCompletedTitle") - .IsRequired() - .HasMaxLength(160) - .HasColumnType("character varying(160)"); - - b.Property("StreamBannerEyebrow") - .IsRequired() - .HasMaxLength(120) - .HasColumnType("character varying(120)"); - - b.Property("StreamBannerLiveButtonLabel") - .IsRequired() - .HasMaxLength(120) - .HasColumnType("character varying(120)"); - - b.Property("StreamBannerLiveButtonUrl") - .IsRequired() - .HasMaxLength(400) - .HasColumnType("character varying(400)"); - - b.Property("StreamBannerLockedButtonLabel") - .IsRequired() - .HasMaxLength(120) - .HasColumnType("character varying(120)"); - - b.Property("StreamBannerText") - .IsRequired() - .HasColumnType("text"); - - b.Property("StreamBannerTitle") - .IsRequired() - .HasMaxLength(160) - .HasColumnType("character varying(160)"); - - b.Property("StreamBannerUseCompletedContent") - .HasColumnType("boolean"); - - b.Property("SubcategoriesSectionDescription") - .IsRequired() - .HasColumnType("text"); - - b.Property("SubcategoriesSectionTitle") - .IsRequired() - .HasColumnType("text"); - - b.Property("TrackingReviewNotes") - .IsRequired() - .HasColumnType("text"); - - b.Property("TrackingRulesJson") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("text") - .HasDefaultValue("[]"); - b.Property("TwitchAuthManagedByDatabase") .HasColumnType("boolean"); @@ -1004,11 +1297,6 @@ namespace Backend.Migrations .HasMaxLength(300) .HasColumnType("character varying(300)"); - b.Property("ViewerStatsProviderBaseUrl") - .IsRequired() - .HasMaxLength(400) - .HasColumnType("character varying(400)"); - b.Property("WorkflowRulesJson") .IsRequired() .ValueGeneratedOnAdd() @@ -1018,6 +1306,58 @@ namespace Backend.Migrations b.HasKey("Id"); b.ToTable("SiteSettings"); + + b.HasData( + new + { + Id = 1, + ClipAdminMenuVisible = true, + ClipReviewEnabled = true, + ClipSubmissionDisabledMessage = "Clip-Einreichungen sind aktuell geschlossen.", + ClipSubmissionsEnabled = false, + ContactContent = "Kontakt zum Award-Team\nDu hast Fragen zur Nominierung, zum Voting, zu Clips oder zur Show? Schreib dem Team ueber die hinterlegte Kontaktseite.\n\nDatenschutzfragen\nFuer Datenschutzanfragen nutze bitte die Datenschutz-E-Mail aus diesem Footer.\n\nCommunity & Kooperationen\nSocial Links und Partneranfragen werden vom Admin-Team gepflegt und koordiniert.", + ContactUrl = "https://vtuber-star-awards.de/kontakt", + DemoLoginDisplayName = "Jayuhime Admin", + DemoLoginEmail = "", + DemoLoginEnabled = false, + DemoLoginManagedByDatabase = false, + DemoLoginPasswordHash = "", + DemoLoginPasswordSalt = "", + DemoLoginTwitchUserId = "jayuhime_admin", + FaqJson = "[{\"question\":\"Wer darf nominiert werden?\",\"answer\":\"Jede:r aktive deutschsprachige VTuber kann nominiert werden \\u2014 unabh\\u00E4ngig von Follower-Zahl oder Plattform. Die Community schl\\u00E4gt in der Nominierungsphase ihre Favorit:innen vor.\"},{\"question\":\"Wie funktioniert das Voting?\",\"answer\":\"Du meldest dich ausschlie\\u00DFlich mit deinem Twitch-Account an \\u2014 nur so kannst du teilnehmen. Das h\\u00E4lt das Voting fair und bot-frei. Danach vergibst du pro Kategorie eine Stimme, \\u00E4nderbar bis zum Ende der Phase.\"},{\"question\":\"Was kostet die Teilnahme?\",\"answer\":\"Gar nichts. Nominieren, voten und die Show schauen ist komplett kostenlos \\u2014 der VTuber Star Award ist ein Community-Event von Fans f\\u00FCr Fans.\"},{\"question\":\"Wann und wo findet die Award-Show statt?\",\"answer\":\"Die gro\\u00DFe Live-Show wird von Jayuhime gehostet und auf Twitch \\u0026 YouTube gestreamt. Den genauen Termin findest du im Countdown oben \\u2014 sei live dabei, wenn die Stars gek\\u00FCrt werden!\"},{\"question\":\"Ich wurde nominiert \\u2014 was nun?\",\"answer\":\"Gl\\u00FCckwunsch! Du erh\\u00E4ltst eine Benachrichtigung mit allen Infos. Teile deine Nominierung gern mit deiner Community und ruf zum Voten auf \\u2014 jede Stimme z\\u00E4hlt.\"}]", + HostDisplayName = "Jayuhime", + HostTagline = "VTuber & Award Host", + ImprintContent = "Anbieter\nVTuber Star Awards, vertreten durch Jayuhime.\n\nKontakt\nNutze fuer organisatorische Fragen bitte die Kontaktseite oder die hinterlegte Kontaktadresse.\n\nHinweis\nDieses Impressum ist ein redaktioneller Platzhalter und sollte vor dem Livegang mit den tatsaechlichen Anbieterangaben ersetzt werden.", + ImprintUrl = "https://vtuber-star-awards.de/impressum", + MaintenanceMessage = "Die Award-Galaxie wird gerade liebevoll poliert. Schau gleich wieder vorbei.", + MaintenanceModeEnabled = false, + MaintenanceTitle = "Sternenpause", + NewsletterUrl = "https://vtuber-star-awards.de/newsletter", + NominationLinkBlacklistJson = "[{\"Url\":\"https://kick.com/\"},{\"Url\":\"https://www.twitch.tv/\"},{\"Url\":\"https://www.youtube.com/\"}]", + PrivacyEmail = "datenschutz@vtuber-star-awards.de", + PrivacyPolicyContent = "Verantwortliche:r\nVTuber Star Awards, vertreten durch Jayuhime. Kontakt: datenschutz@vtuber-star-awards.de\n\nWelche Daten wir verarbeiten\nBei Teilnahme (Voting, Nominierung, Clip-Einreichung) verarbeiten wir ausschließlich deine Twitch-User-ID sowie den Zeitstempel deiner Aktion.\nFür Show-Erinnerungen speichern wir optional deine E-Mail-Adresse.\n\nRechtsgrundlage\nVerarbeitung auf Basis von Art. 6 Abs. 1 lit. b DSGVO. Für optionale Erinnerungen gilt Art. 6 Abs. 1 lit. a DSGVO.\n\nZweck der Verarbeitung\nDurchführung des VTuber Star Awards, Sicherstellung fairer Abstimmung, Spam-Prävention sowie Admin-Review von Einreichungen.\n\nLöschfristen\nAlle Teilnahmedaten werden spätestens 6 Monate nach der Award-Show automatisch gelöscht. E-Mail-Adressen werden nach der Show entfernt.\n\nDeine Rechte\nDu hast das Recht auf Auskunft, Löschung, Berichtigung, Widerspruch und Beschwerde bei der zuständigen Aufsichtsbehörde.\n\nWeitergabe an Dritte\nKeine Weitergabe zu Werbezwecken. Technische Dienstleister verarbeiten Daten ausschließlich im Rahmen der Auftragsverarbeitung.", + PrivacyPolicyUpdatedAt = new DateTimeOffset(new DateTime(2026, 6, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + PrivacyPolicyUpdatedBy = "seed", + RiskRulesJson = "[{\"key\":\"resubmitted_ballot\",\"label\":\"Ballot erneut gespeichert\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"low\",\"description\":\"Erstellt einen Hinweis, wenn ein User sein Voting erneut speichert.\"},{\"key\":\"rapid_vote_updates\",\"label\":\"Voting-Burst\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr schnell mehrere Voting-Aenderungen ausloest.\"},{\"key\":\"resubmitted_nomination\",\"label\":\"Nominierung erneut eingereicht\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"low\",\"description\":\"Erstellt einen Hinweis, wenn ein User in derselben Kategorie erneut nominiert.\"},{\"key\":\"rapid_nomination_burst\",\"label\":\"Nominierungs-Burst\",\"enabled\":true,\"threshold\":10,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr viele Nominierungen in kurzer Zeit sendet.\"},{\"key\":\"duplicate_clip_submission\",\"label\":\"Doppelter Clip\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn ein User denselben Clip erneut einreicht.\"},{\"key\":\"rapid_clip_burst\",\"label\":\"Clip-Burst\",\"enabled\":true,\"threshold\":5,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr viele Clips in kurzer Zeit sendet.\"},{\"key\":\"rapid_login_ip\",\"label\":\"Login-Burst pro IP\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":15,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn mehrere neue Sessions von derselben IP entstehen.\"},{\"key\":\"rapid_demo_login_ip\",\"label\":\"Demo-Login-Burst pro IP\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":15,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn mehrere Demo-Admin-Sessions von derselben IP entstehen.\"}]", + SessionIdleTimeoutHours = 3, + ShareDiscordUrl = "", + ShareXUrl = "", + ShowactApplicationDisabledMessage = "Showact-Bewerbungen sind aktuell geschlossen.", + ShowactApplicationsEnabled = false, + ShowactFormSchemaJson = "[]", + ShowactsContent = "Informationen zu Showact-Bewerbungen, Ablauf und Kontakt fuer die Award-Show.", + ShowactsUrl = "https://vtuber-star-awards.de/showacts", + SocialLinksJson = "[{\"label\":\"Twitch\",\"platform\":\"twitch\",\"url\":\"https://twitch.tv/jayuhime\",\"icon\":\"twitch\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"YouTube\",\"platform\":\"youtube\",\"url\":\"https://youtube.com/c/Jayuhime\",\"icon\":\"youtube\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"X\",\"platform\":\"x\",\"url\":\"https://x.com/jayuhime\",\"icon\":\"x\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Instagram\",\"platform\":\"instagram\",\"url\":\"https://instagram.com/jayuhime\",\"icon\":\"instagram\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Discord\",\"platform\":\"discord\",\"url\":\"https://discord.gg/jayuhime\",\"icon\":\"discord\",\"showOnHost\":true,\"showOnCommunity\":true}]", + SponsorsContent = "Sponsoren & Partner\nHier koennen Sponsor:innen, Medienpartner, Community-Partner und Supporter des VTuber Star Awards vorgestellt werden.\n\nPartner werden im Rahmen der Show und auf den oeffentlichen Kontaktflaechen genannt, sobald sie final bestaetigt sind.", + SponsorsUrl = "https://vtuber-star-awards.de/partner", + SponsorsVisible = true, + TwitchAuthManagedByDatabase = false, + TwitchClientId = "", + TwitchClientSecret = "", + TwitchRedirectUri = "", + TwitchScope = "", + WorkflowRulesJson = "[{\"key\":\"max_finalists_per_category\",\"label\":\"Finale Kandidat:innen pro Kategorie\",\"enabled\":true,\"limit\":4,\"mode\":\"block\",\"description\":\"Begrenzt, wie viele finale Kandidat:innen in einer Kategorie vorbereitet werden.\"},{\"key\":\"max_candidate_appearances\",\"label\":\"Kandidaturen pro Person\",\"enabled\":true,\"limit\":2,\"mode\":\"warn\",\"description\":\"Warnt oder blockiert, wenn dieselbe Person in zu vielen Kategorien final auftaucht.\"},{\"key\":\"max_winner_placements\",\"label\":\"Gewinnerpl\\u00E4tze pro Person\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn dieselbe Person mehrfach als Gewinner:in gesetzt wird.\"},{\"key\":\"winner_requires_clip\",\"label\":\"Gewinner braucht Clip-Link\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn ein Gewinner ohne gepflegten YouTube-/Twitch-Clip gesetzt wird.\"}]" + }); }); modelBuilder.Entity("Backend.Domain.Sponsor", b => @@ -1316,6 +1656,24 @@ namespace Backend.Migrations .IsUnique(); b.ToTable("VoteBallots"); + + b.HasData( + new + { + Id = 1, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_1" + }, + new + { + Id = 2, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 5, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_2" + }); }); modelBuilder.Entity("Backend.Domain.VoteEntry", b => @@ -1344,6 +1702,36 @@ namespace Backend.Migrations b.HasIndex("CategoryId"); b.ToTable("VoteEntries"); + + b.HasData( + new + { + Id = 1, + BallotId = 1, + CandidateId = 1, + CategoryId = 1 + }, + new + { + Id = 2, + BallotId = 1, + CandidateId = 4, + CategoryId = 2 + }, + new + { + Id = 3, + BallotId = 2, + CandidateId = 2, + CategoryId = 1 + }, + new + { + Id = 4, + BallotId = 2, + CandidateId = 6, + CategoryId = 3 + }); }); modelBuilder.Entity("Backend.Domain.AwardResult", b => @@ -1416,12 +1804,6 @@ namespace Backend.Migrations .HasForeignKey("CandidateId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("Backend.Domain.Season", null) - .WithMany() - .HasForeignKey("SeasonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - b.Navigation("Candidate"); }); diff --git a/Backend/Migrations/20260628161949_AddShowactApplicationSchedule.cs b/Backend/Migrations/20260628161949_AddShowactApplicationSchedule.cs new file mode 100644 index 0000000..2baeac0 --- /dev/null +++ b/Backend/Migrations/20260628161949_AddShowactApplicationSchedule.cs @@ -0,0 +1,46 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backend.Migrations +{ + /// + public partial class AddShowactApplicationSchedule : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ShowactApplicationEndsAt", + table: "SiteSettings", + type: "date", + nullable: true); + + migrationBuilder.AddColumn( + name: "ShowactApplicationStartsAt", + table: "SiteSettings", + type: "date", + nullable: true); + + migrationBuilder.UpdateData( + table: "SiteSettings", + keyColumn: "Id", + keyValue: 1, + columns: new[] { "ShowactApplicationEndsAt", "ShowactApplicationStartsAt" }, + values: new object[] { null, null }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ShowactApplicationEndsAt", + table: "SiteSettings"); + + migrationBuilder.DropColumn( + name: "ShowactApplicationStartsAt", + table: "SiteSettings"); + } + } +} diff --git a/Backend/Migrations/20260628205353_AddSeasonWorkflowRulesJson.Designer.cs b/Backend/Migrations/20260628205353_AddSeasonWorkflowRulesJson.Designer.cs new file mode 100644 index 0000000..6c61020 --- /dev/null +++ b/Backend/Migrations/20260628205353_AddSeasonWorkflowRulesJson.Designer.cs @@ -0,0 +1,2010 @@ +// +using System; +using Backend.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backend.Migrations +{ + [DbContext(typeof(AwardsDbContext))] + [Migration("20260628205353_AddSeasonWorkflowRulesJson")] + partial class AddSeasonWorkflowRulesJson + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backend.Domain.AdminAuditEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("AdminTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.ToTable("AdminAuditEntries"); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CategoryName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId", "CategoryId") + .IsUnique(); + + b.ToTable("Results"); + + b.HasData( + new + { + Id = 1, + CandidateId = 8, + CategoryId = 5, + CategoryName = "VTuber des Jahres", + SeasonId = 2 + }, + new + { + Id = 2, + CandidateId = 9, + CategoryId = 6, + CategoryName = "Bestes Live Event", + SeasonId = 2 + }, + new + { + Id = 3, + CandidateId = 10, + CategoryId = 7, + CategoryName = "Clip des Jahres", + SeasonId = 2 + }, + new + { + Id = 4, + CandidateId = 11, + CategoryId = 8, + CategoryName = "VTuber des Jahres", + SeasonId = 3 + }, + new + { + Id = 5, + CandidateId = 12, + CategoryId = 9, + CategoryName = "Clip des Jahres", + SeasonId = 3 + }, + new + { + Id = 6, + CandidateId = 13, + CategoryId = 10, + CategoryName = "VTuber des Jahres", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AcceptanceNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AcceptanceStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasDefaultValue("open"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ChannelSlug") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ClipCompilationPlatform") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ClipCompilationTitle") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ClipCompilationUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ClipEmbedStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasDefaultValue("unchecked"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("NominationTally") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("StreamerIdentityId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("SeasonId"); + + b.HasIndex("StreamerIdentityId"); + + b.ToTable("Candidates"); + + b.HasData( + new + { + Id = 1, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@hoshimimiyu", + ClipEmbedStatus = "unchecked", + DisplayName = "Hoshimi Miyu", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 2, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 3, + AcceptanceStatus = "open", + CategoryId = 1, + ChannelSlug = "@shiroch", + ClipEmbedStatus = "unchecked", + DisplayName = "Shiro Ch.", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 4, + AcceptanceStatus = "open", + CategoryId = 2, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu 3D Live", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 5, + AcceptanceStatus = "open", + CategoryId = 2, + ChannelSlug = "@aoisakura", + ClipEmbedStatus = "unchecked", + DisplayName = "Aoi Sakura Showcase", + NominationTally = 0, + Platform = "YouTube", + SeasonId = 1 + }, + new + { + Id = 6, + AcceptanceStatus = "open", + CategoryId = 3, + ChannelSlug = "@pyonkichikingdom", + ClipEmbedStatus = "unchecked", + DisplayName = "Pyonkichi Kingdom", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 7, + AcceptanceStatus = "open", + CategoryId = 4, + ChannelSlug = "@moonrelay", + ClipEmbedStatus = "unchecked", + DisplayName = "Moonrelay", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 1 + }, + new + { + Id = 8, + AcceptanceStatus = "open", + CategoryId = 5, + ChannelSlug = "@hoshimimiyu", + ClipEmbedStatus = "unchecked", + DisplayName = "Hoshimi Miyu", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 9, + AcceptanceStatus = "open", + CategoryId = 6, + ChannelSlug = "@kurainu", + ClipEmbedStatus = "unchecked", + DisplayName = "Kurainu 3D Live", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 10, + AcceptanceStatus = "open", + CategoryId = 7, + ChannelSlug = "@pyonkichikingdom", + ClipEmbedStatus = "unchecked", + DisplayName = "Pyonkichi Kingdom", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 2 + }, + new + { + Id = 11, + AcceptanceStatus = "open", + CategoryId = 8, + ChannelSlug = "@aoisakura", + ClipEmbedStatus = "unchecked", + DisplayName = "Aoi Sakura", + NominationTally = 0, + Platform = "YouTube", + SeasonId = 3 + }, + new + { + Id = 12, + AcceptanceStatus = "open", + CategoryId = 9, + ChannelSlug = "@starbyte", + ClipEmbedStatus = "unchecked", + DisplayName = "Starbyte", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 3 + }, + new + { + Id = 13, + AcceptanceStatus = "open", + CategoryId = 10, + ChannelSlug = "@tenshivox", + ClipEmbedStatus = "unchecked", + DisplayName = "Tenshi Vox", + NominationTally = 0, + Platform = "Twitch", + SeasonId = 4 + }); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MaxNomineesPerUser") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("ViewerRangeMax") + .HasColumnType("integer"); + + b.Property("ViewerRangeMin") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Slug") + .IsUnique(); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = 1, + Description = "Die größte Auszeichnung des Jahres.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 1, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 2, + Description = "Events, Konzerte und 3D-Shows.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 1, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 3, + Description = "Der lustigste oder emotionalste Clip des Jahres.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 1, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 4, + Description = "Die aktivste und freundlichste Community.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "Beste Community", + SeasonId = 1, + Slug = "beste-community", + SortOrder = 4 + }, + new + { + Id = 5, + Description = "Archivkategorie 2025.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 2, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 6, + Description = "Archivkategorie 2025.", + GroupName = "Performance", + MaxNomineesPerUser = 3, + Name = "Bestes Live Event", + SeasonId = 2, + Slug = "bestes-live-event", + SortOrder = 2 + }, + new + { + Id = 7, + Description = "Archivkategorie 2025.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 2, + Slug = "clip-des-jahres", + SortOrder = 3 + }, + new + { + Id = 8, + Description = "Archivkategorie 2024.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 3, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }, + new + { + Id = 9, + Description = "Archivkategorie 2024.", + GroupName = "Clips & Highlights", + MaxNomineesPerUser = 3, + Name = "Clip des Jahres", + SeasonId = 3, + Slug = "clip-des-jahres", + SortOrder = 2 + }, + new + { + Id = 10, + Description = "Archivkategorie 2023.", + GroupName = "Main Awards", + MaxNomineesPerUser = 3, + Name = "VTuber des Jahres", + SeasonId = 4, + Slug = "vtuber-des-jahres", + SortOrder = 1 + }); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("ClipUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Creator") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("ClipSubmissions"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AvgViewers") + .HasColumnType("integer"); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CandidateText") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CategoryGroupName") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(80) + .HasColumnType("character varying(80)") + .HasDefaultValue(""); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FollowersGained") + .HasColumnType("integer"); + + b.Property("HoursStreamed") + .HasColumnType("integer"); + + b.Property("HoursWatched") + .HasColumnType("integer"); + + b.Property("PeakViewers") + .HasColumnType("integer"); + + b.Property("ResolvedChannel") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ResolvedPlatform") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("StreamUrl") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("StreamerIdentityId") + .HasColumnType("integer"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SuggestedCategoryId") + .HasColumnType("integer"); + + b.Property("TrackerCheckedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TrackerStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasDefaultValue("pending"); + + b.Property("TrackingFlagsJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("[]"); + + b.Property("TrackingReviewNote") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("TrackingReviewStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasDefaultValue("clear"); + + b.Property("TrackingReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TrackingReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("StreamerIdentityId"); + + b.HasIndex("SuggestedCategoryId"); + + b.HasIndex("SeasonId", "Status"); + + b.HasIndex("SeasonId", "CategoryGroupName", "Status"); + + b.HasIndex("SeasonId", "StreamerIdentityId", "CategoryGroupName"); + + b.ToTable("Nominations"); + + b.HasData( + new + { + Id = 1, + CandidateText = "Hoshimi Miyu", + CategoryGroupName = "", + CategoryId = 1, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 13, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_hoshi", + TrackerStatus = "pending", + TrackingFlagsJson = "[]", + TrackingReviewStatus = "clear" + }, + new + { + Id = 2, + CandidateText = "Kurainu 3D Live", + CategoryGroupName = "", + CategoryId = 2, + CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 14, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SeasonId = 1, + Status = "pending", + SubmittedByTwitchId = "twitch_kurainu", + TrackerStatus = "pending", + TrackingFlagsJson = "[]", + TrackingReviewStatus = "clear" + }); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("TwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("RiskFlags"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrentPhase") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("IsCommunityOnly") + .HasColumnType("boolean"); + + b.Property("IsCurrent") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("NominationEndsAt") + .HasColumnType("date"); + + b.Property("NominationStartsAt") + .HasColumnType("date"); + + b.Property("ReviewEndsAt") + .HasColumnType("date"); + + b.Property("ReviewStartsAt") + .HasColumnType("date"); + + b.Property("ShowDate") + .HasColumnType("date"); + + b.Property("ShowStartsAt") + .HasColumnType("time without time zone"); + + b.Property("ShowStreamUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("SubcategoryTemplatesJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("[]"); + + b.Property("VotingEndsAt") + .HasColumnType("date"); + + b.Property("VotingStartsAt") + .HasColumnType("date"); + + b.Property("WorkflowRulesJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("[]"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("Seasons"); + + b.HasData( + new + { + Id = 1, + CurrentPhase = "Community Voting", + IsCommunityOnly = true, + IsCurrent = true, + Name = "VTuber Star Awards 2026", + NominationEndsAt = new DateOnly(2026, 5, 31), + NominationStartsAt = new DateOnly(2026, 5, 1), + ReviewEndsAt = new DateOnly(2026, 7, 10), + ReviewStartsAt = new DateOnly(2026, 7, 1), + ShowDate = new DateOnly(2026, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + SubcategoryTemplatesJson = "[]", + VotingEndsAt = new DateOnly(2026, 6, 30), + VotingStartsAt = new DateOnly(2026, 6, 1), + WorkflowRulesJson = "[]", + Year = 2026 + }, + new + { + Id = 2, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2025", + NominationEndsAt = new DateOnly(2025, 5, 31), + NominationStartsAt = new DateOnly(2025, 5, 1), + ReviewEndsAt = new DateOnly(2025, 7, 10), + ReviewStartsAt = new DateOnly(2025, 7, 1), + ShowDate = new DateOnly(2025, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + SubcategoryTemplatesJson = "[]", + VotingEndsAt = new DateOnly(2025, 6, 30), + VotingStartsAt = new DateOnly(2025, 6, 1), + WorkflowRulesJson = "[]", + Year = 2025 + }, + new + { + Id = 3, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2024", + NominationEndsAt = new DateOnly(2024, 5, 31), + NominationStartsAt = new DateOnly(2024, 5, 1), + ReviewEndsAt = new DateOnly(2024, 7, 10), + ReviewStartsAt = new DateOnly(2024, 7, 1), + ShowDate = new DateOnly(2024, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://youtube.com/c/Jayuhime", + SubcategoryTemplatesJson = "[]", + VotingEndsAt = new DateOnly(2024, 6, 30), + VotingStartsAt = new DateOnly(2024, 6, 1), + WorkflowRulesJson = "[]", + Year = 2024 + }, + new + { + Id = 4, + CurrentPhase = "Archived", + IsCommunityOnly = true, + IsCurrent = false, + Name = "VTuber Star Awards 2023", + NominationEndsAt = new DateOnly(2023, 5, 31), + NominationStartsAt = new DateOnly(2023, 5, 1), + ReviewEndsAt = new DateOnly(2023, 7, 10), + ReviewStartsAt = new DateOnly(2023, 7, 1), + ShowDate = new DateOnly(2023, 7, 20), + ShowStartsAt = new TimeOnly(20, 0, 0), + ShowStreamUrl = "https://twitch.tv/jayuhime", + SubcategoryTemplatesJson = "[]", + VotingEndsAt = new DateOnly(2023, 6, 30), + VotingStartsAt = new DateOnly(2023, 6, 1), + WorkflowRulesJson = "[]", + Year = 2023 + }); + }); + + modelBuilder.Entity("Backend.Domain.ShowactApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ArtistName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ContactDiscord") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ContactEmail") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("FieldResponsesJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("PerformanceType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("PlatformUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReferenceUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TechnicalNotes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Status"); + + b.ToTable("ShowactApplications"); + }); + + modelBuilder.Entity("Backend.Domain.SiteSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClipAdminMenuVisible") + .HasColumnType("boolean"); + + b.Property("ClipReviewEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ClipSubmissionDisabledMessage") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("ClipSubmissionsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ContactContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("DemoLoginDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginEmail") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("DemoLoginEnabled") + .HasColumnType("boolean"); + + b.Property("DemoLoginManagedByDatabase") + .HasColumnType("boolean"); + + b.Property("DemoLoginPasswordHash") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DemoLoginPasswordSalt") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DemoLoginTwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("FaqJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("HostDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("HostTagline") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("ImprintContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("ImprintUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("MaintenanceMessage") + .IsRequired() + .HasMaxLength(600) + .HasColumnType("character varying(600)"); + + b.Property("MaintenanceModeEnabled") + .HasColumnType("boolean"); + + b.Property("MaintenanceTitle") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("NewsletterUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("NominationLinkBlacklistJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("[]"); + + b.Property("PrivacyEmail") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("PrivacyPolicyContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("PrivacyPolicyUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivacyPolicyUpdatedBy") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("RiskRulesJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SessionIdleTimeoutHours") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(3); + + b.Property("ShareDiscordUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShareXUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShowactApplicationDisabledMessage") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("ShowactApplicationEndsAt") + .HasColumnType("date"); + + b.Property("ShowactApplicationStartsAt") + .HasColumnType("date"); + + b.Property("ShowactApplicationsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ShowactFormSchemaJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShowactsContent") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue(""); + + b.Property("ShowactsUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("SocialLinksJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("SponsorsUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("SponsorsVisible") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("TrackingReviewNotes") + .IsRequired() + .HasColumnType("text"); + + b.Property("TrackingRulesJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("[]"); + + b.Property("TwitchAuthManagedByDatabase") + .HasColumnType("boolean"); + + b.Property("TwitchClientId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchClientSecret") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("TwitchRedirectUri") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("TwitchScope") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("ViewerStatsProviderBaseUrl") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("WorkflowRulesJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("[]"); + + b.HasKey("Id"); + + b.ToTable("SiteSettings"); + + b.HasData( + new + { + Id = 1, + ClipAdminMenuVisible = true, + ClipReviewEnabled = true, + ClipSubmissionDisabledMessage = "Clip-Einreichungen sind aktuell geschlossen.", + ClipSubmissionsEnabled = false, + ContactContent = "Kontakt zum Award-Team\nDu hast Fragen zur Nominierung, zum Voting, zu Clips oder zur Show? Schreib dem Team über die hinterlegte Kontaktseite.\n\nDatenschutzfragen\nFür Datenschutzanfragen nutze bitte die Datenschutz-E-Mail aus diesem Footer.\n\nCommunity & Kooperationen\nSocial Links und Partneranfragen werden vom Admin-Team gepflegt und koordiniert.", + ContactUrl = "https://vtuber-star-awards.de/kontakt", + DemoLoginDisplayName = "Jayuhime Admin", + DemoLoginEmail = "", + DemoLoginEnabled = false, + DemoLoginManagedByDatabase = false, + DemoLoginPasswordHash = "", + DemoLoginPasswordSalt = "", + DemoLoginTwitchUserId = "jayuhime_admin", + FaqJson = "[{\"question\":\"Wer darf nominiert werden?\",\"answer\":\"Jede:r aktive deutschsprachige VTuber kann nominiert werden \\u2014 unabh\\u00E4ngig von Follower-Zahl oder Plattform. Die Community schl\\u00E4gt in der Nominierungsphase ihre Favorit:innen vor.\"},{\"question\":\"Wie funktioniert das Voting?\",\"answer\":\"Du meldest dich ausschlie\\u00DFlich mit deinem Twitch-Account an \\u2014 nur so kannst du teilnehmen. Das h\\u00E4lt das Voting fair und bot-frei. Danach vergibst du pro Kategorie eine Stimme, \\u00E4nderbar bis zum Ende der Phase.\"},{\"question\":\"Was kostet die Teilnahme?\",\"answer\":\"Gar nichts. Nominieren, voten und die Show schauen ist komplett kostenlos \\u2014 der VTuber Star Award ist ein Community-Event von Fans f\\u00FCr Fans.\"},{\"question\":\"Wann und wo findet die Award-Show statt?\",\"answer\":\"Die gro\\u00DFe Live-Show wird von Jayuhime gehostet und auf Twitch \\u0026 YouTube gestreamt. Den genauen Termin findest du im Countdown oben \\u2014 sei live dabei, wenn die Stars gek\\u00FCrt werden!\"},{\"question\":\"Ich wurde nominiert \\u2014 was nun?\",\"answer\":\"Gl\\u00FCckwunsch! Du erh\\u00E4ltst eine Benachrichtigung mit allen Infos. Teile deine Nominierung gern mit deiner Community und ruf zum Voten auf \\u2014 jede Stimme z\\u00E4hlt.\"}]", + HostDisplayName = "Jayuhime", + HostTagline = "VTuber & Award Host", + ImprintContent = "Anbieter\nVTuber Star Awards, vertreten durch Jayuhime.\n\nKontakt\nNutze für organisatorische Fragen bitte die Kontaktseite oder die hinterlegte Kontaktadresse.\n\nHinweis\nDieses Impressum ist ein redaktioneller Platzhalter und sollte vor dem Livegang mit den tatsächlichen Anbieterangaben ersetzt werden.", + ImprintUrl = "https://vtuber-star-awards.de/impressum", + MaintenanceMessage = "Die Award-Galaxie wird gerade liebevoll poliert. Schau gleich wieder vorbei.", + MaintenanceModeEnabled = false, + MaintenanceTitle = "Sternenpause", + NewsletterUrl = "https://vtuber-star-awards.de/newsletter", + NominationLinkBlacklistJson = "[{\"Url\":\"https://kick.com/\"},{\"Url\":\"https://www.twitch.tv/\"},{\"Url\":\"https://www.youtube.com/\"}]", + PrivacyEmail = "datenschutz@vtuber-star-awards.de", + PrivacyPolicyContent = "Verantwortliche:r\nVTuber Star Awards, vertreten durch Jayuhime. Kontakt: datenschutz@vtuber-star-awards.de\n\nWelche Daten wir verarbeiten\nBei Teilnahme (Voting, Nominierung, Clip-Einreichung) verarbeiten wir ausschließlich deine Twitch-User-ID sowie den Zeitstempel deiner Aktion.\nFür Show-Erinnerungen speichern wir optional deine E-Mail-Adresse.\n\nRechtsgrundlage\nVerarbeitung auf Basis von Art. 6 Abs. 1 lit. b DSGVO. Für optionale Erinnerungen gilt Art. 6 Abs. 1 lit. a DSGVO.\n\nZweck der Verarbeitung\nDurchführung des VTuber Star Awards, Sicherstellung fairer Abstimmung, Spam-Prävention sowie Admin-Review von Einreichungen.\n\nLöschfristen\nAlle Teilnahmedaten werden spätestens 6 Monate nach der Award-Show automatisch gelöscht. E-Mail-Adressen werden nach der Show entfernt.\n\nDeine Rechte\nDu hast das Recht auf Auskunft, Löschung, Berichtigung, Widerspruch und Beschwerde bei der zuständigen Aufsichtsbehörde.\n\nWeitergabe an Dritte\nKeine Weitergabe zu Werbezwecken. Technische Dienstleister verarbeiten Daten ausschließlich im Rahmen der Auftragsverarbeitung.", + PrivacyPolicyUpdatedAt = new DateTimeOffset(new DateTime(2026, 6, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + PrivacyPolicyUpdatedBy = "seed", + RiskRulesJson = "[{\"key\":\"resubmitted_ballot\",\"label\":\"Ballot erneut gespeichert\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"low\",\"description\":\"Erstellt einen Hinweis, wenn ein User sein Voting erneut speichert.\"},{\"key\":\"rapid_vote_updates\",\"label\":\"Voting-Burst\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr schnell mehrere Voting-Aenderungen ausloest.\"},{\"key\":\"resubmitted_nomination\",\"label\":\"Nominierung erneut eingereicht\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"low\",\"description\":\"Erstellt einen Hinweis, wenn ein User in derselben Kategorie erneut nominiert.\"},{\"key\":\"rapid_nomination_burst\",\"label\":\"Nominierungs-Burst\",\"enabled\":true,\"threshold\":10,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr viele Nominierungen in kurzer Zeit sendet.\"},{\"key\":\"duplicate_clip_submission\",\"label\":\"Doppelter Clip\",\"enabled\":true,\"threshold\":1,\"windowMinutes\":360,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn ein User denselben Clip erneut einreicht.\"},{\"key\":\"rapid_clip_burst\",\"label\":\"Clip-Burst\",\"enabled\":true,\"threshold\":5,\"windowMinutes\":10,\"severity\":\"high\",\"description\":\"Erstellt einen Hinweis, wenn ein User sehr viele Clips in kurzer Zeit sendet.\"},{\"key\":\"rapid_login_ip\",\"label\":\"Login-Burst pro IP\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":15,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn mehrere neue Sessions von derselben IP entstehen.\"},{\"key\":\"rapid_demo_login_ip\",\"label\":\"Demo-Login-Burst pro IP\",\"enabled\":true,\"threshold\":3,\"windowMinutes\":15,\"severity\":\"medium\",\"description\":\"Erstellt einen Hinweis, wenn mehrere Demo-Admin-Sessions von derselben IP entstehen.\"}]", + SessionIdleTimeoutHours = 3, + ShareDiscordUrl = "", + ShareXUrl = "", + ShowactApplicationDisabledMessage = "Showact-Bewerbungen sind aktuell geschlossen.", + ShowactApplicationsEnabled = false, + ShowactFormSchemaJson = "[]", + ShowactsContent = "Informationen zu Showact-Bewerbungen, Ablauf und Kontakt für die Award-Show.", + ShowactsUrl = "https://vtuber-star-awards.de/showacts", + SocialLinksJson = "[{\"label\":\"Twitch\",\"platform\":\"twitch\",\"url\":\"https://twitch.tv/jayuhime\",\"icon\":\"twitch\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"YouTube\",\"platform\":\"youtube\",\"url\":\"https://youtube.com/c/Jayuhime\",\"icon\":\"youtube\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"X\",\"platform\":\"x\",\"url\":\"https://x.com/jayuhime\",\"icon\":\"x\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Instagram\",\"platform\":\"instagram\",\"url\":\"https://instagram.com/jayuhime\",\"icon\":\"instagram\",\"showOnHost\":true,\"showOnCommunity\":true},{\"label\":\"Discord\",\"platform\":\"discord\",\"url\":\"https://discord.gg/jayuhime\",\"icon\":\"discord\",\"showOnHost\":true,\"showOnCommunity\":true}]", + SponsorsContent = "Sponsoren & Partner\nHier können Sponsor:innen, Medienpartner, Community-Partner und Supporter des VTuber Star Awards vorgestellt werden.\n\nPartner werden im Rahmen der Show und auf den öffentlichen Kontaktflächen genannt, sobald sie final bestätigt sind.", + SponsorsUrl = "https://vtuber-star-awards.de/partner", + SponsorsVisible = true, + TrackingReviewNotes = "Fallback-Quellen für manuelle Reviews:\\n- SullyGnome\\n- Twitch-Kanal direkt\\n\\nNutze diese Notizen für Edge Cases und manuelle Tier-Entscheidungen.", + TrackingRulesJson = "{\"source\":{\"providerKey\":\"twitchtracker\",\"baseUrl\":\"https://twitchtracker.com/api\",\"notesSummary\":\"TwitchTracker Basic API liefert aktuell Channel-Summary-Daten fuer 30 Tage. Andere Zeitfenster bleiben konfigurierbar, werden aber als manueller Review-Fall markiert.\",\"showManualReviewNotesInReview\":true},\"importantMetrics\":[{\"key\":\"avg_viewers\",\"label\":\"Avg Viewer\",\"enabled\":true,\"sourceSupport\":\"auto\",\"description\":\"Durchschnittliche Viewer fuer den gewaehlten Zeitraum.\",\"requiredForAutoClassification\":true,\"showInReview\":true,\"showInAdminSummary\":true,\"manualOverrideAllowed\":true,\"windowKey\":\"90d\",\"autoSupportedWindowKeys\":[\"30d\"],\"providerFieldKey\":\"avg_viewers\",\"topCount\":null,\"minPrimaryCategorySharePercent\":null,\"minPrimaryCategoryHours\":null,\"maxDistinctCategoriesBeforeFlag\":null,\"ignoredCategories\":[],\"matchAwardCategoryAgainstTopCategories\":false,\"flagIfAwardCategoryNotInTopX\":false,\"flagIfCategorySpreadTooWide\":false,\"flagIfNoCategoryContextAvailable\":false,\"minValue\":null,\"maxValue\":null},{\"key\":\"tracker_status\",\"label\":\"Tracker-Status\",\"enabled\":true,\"sourceSupport\":\"auto\",\"description\":\"Zeigt, ob der TwitchTracker-Lookup sauber aufgeloest werden konnte.\",\"requiredForAutoClassification\":true,\"showInReview\":true,\"showInAdminSummary\":true,\"manualOverrideAllowed\":false,\"windowKey\":\"30d\",\"autoSupportedWindowKeys\":[\"30d\"],\"providerFieldKey\":\"tracker_status\",\"topCount\":null,\"minPrimaryCategorySharePercent\":null,\"minPrimaryCategoryHours\":null,\"maxDistinctCategoriesBeforeFlag\":null,\"ignoredCategories\":[],\"matchAwardCategoryAgainstTopCategories\":false,\"flagIfAwardCategoryNotInTopX\":false,\"flagIfCategorySpreadTooWide\":false,\"flagIfNoCategoryContextAvailable\":false,\"minValue\":null,\"maxValue\":null},{\"key\":\"tracker_checked_at\",\"label\":\"Letzter Tracker-Check\",\"enabled\":true,\"sourceSupport\":\"auto\",\"description\":\"Zeitpunkt der letzten automatischen Datenaufloesung.\",\"requiredForAutoClassification\":true,\"showInReview\":true,\"showInAdminSummary\":false,\"manualOverrideAllowed\":false,\"windowKey\":\"30d\",\"autoSupportedWindowKeys\":[\"30d\"],\"providerFieldKey\":\"tracker_checked_at\",\"topCount\":null,\"minPrimaryCategorySharePercent\":null,\"minPrimaryCategoryHours\":null,\"maxDistinctCategoriesBeforeFlag\":null,\"ignoredCategories\":[],\"matchAwardCategoryAgainstTopCategories\":false,\"flagIfAwardCategoryNotInTopX\":false,\"flagIfCategorySpreadTooWide\":false,\"flagIfNoCategoryContextAvailable\":false,\"minValue\":null,\"maxValue\":null}],\"optionalMetrics\":[{\"key\":\"hours_streamed\",\"label\":\"Hours Streamed\",\"enabled\":true,\"sourceSupport\":\"auto\",\"description\":\"Gesamte Streamstunden im gewaehlten Zeitraum.\",\"requiredForAutoClassification\":false,\"showInReview\":true,\"showInAdminSummary\":true,\"manualOverrideAllowed\":true,\"windowKey\":\"30d\",\"autoSupportedWindowKeys\":[\"30d\"],\"providerFieldKey\":\"hours_streamed\",\"topCount\":null,\"minPrimaryCategorySharePercent\":null,\"minPrimaryCategoryHours\":null,\"maxDistinctCategoriesBeforeFlag\":null,\"ignoredCategories\":[],\"matchAwardCategoryAgainstTopCategories\":false,\"flagIfAwardCategoryNotInTopX\":false,\"flagIfCategorySpreadTooWide\":false,\"flagIfNoCategoryContextAvailable\":false,\"minValue\":null,\"maxValue\":null},{\"key\":\"hours_watched\",\"label\":\"Hours Watched\",\"enabled\":true,\"sourceSupport\":\"auto\",\"description\":\"Gesamte Watch Time fuer den gewaehlten Zeitraum.\",\"requiredForAutoClassification\":false,\"showInReview\":true,\"showInAdminSummary\":true,\"manualOverrideAllowed\":true,\"windowKey\":\"30d\",\"autoSupportedWindowKeys\":[\"30d\"],\"providerFieldKey\":\"hours_watched\",\"topCount\":null,\"minPrimaryCategorySharePercent\":null,\"minPrimaryCategoryHours\":null,\"maxDistinctCategoriesBeforeFlag\":null,\"ignoredCategories\":[],\"matchAwardCategoryAgainstTopCategories\":false,\"flagIfAwardCategoryNotInTopX\":false,\"flagIfCategorySpreadTooWide\":false,\"flagIfNoCategoryContextAvailable\":false,\"minValue\":null,\"maxValue\":null},{\"key\":\"peak_viewers\",\"label\":\"Peak Viewer\",\"enabled\":true,\"sourceSupport\":\"auto\",\"description\":\"Hoechster gleichzeitiger Zuschauerwert im Zeitraum.\",\"requiredForAutoClassification\":false,\"showInReview\":true,\"showInAdminSummary\":true,\"manualOverrideAllowed\":true,\"windowKey\":\"30d\",\"autoSupportedWindowKeys\":[\"30d\"],\"providerFieldKey\":\"peak_viewers\",\"topCount\":null,\"minPrimaryCategorySharePercent\":null,\"minPrimaryCategoryHours\":null,\"maxDistinctCategoriesBeforeFlag\":null,\"ignoredCategories\":[],\"matchAwardCategoryAgainstTopCategories\":false,\"flagIfAwardCategoryNotInTopX\":false,\"flagIfCategorySpreadTooWide\":false,\"flagIfNoCategoryContextAvailable\":false,\"minValue\":null,\"maxValue\":null},{\"key\":\"followers_gained\",\"label\":\"Follower Growth\",\"enabled\":true,\"sourceSupport\":\"auto\",\"description\":\"Follower-Zuwachs im gewaehlten Zeitraum.\",\"requiredForAutoClassification\":false,\"showInReview\":true,\"showInAdminSummary\":true,\"manualOverrideAllowed\":true,\"windowKey\":\"30d\",\"autoSupportedWindowKeys\":[\"30d\"],\"providerFieldKey\":\"followers_gained\",\"topCount\":null,\"minPrimaryCategorySharePercent\":null,\"minPrimaryCategoryHours\":null,\"maxDistinctCategoriesBeforeFlag\":null,\"ignoredCategories\":[],\"matchAwardCategoryAgainstTopCategories\":false,\"flagIfAwardCategoryNotInTopX\":false,\"flagIfCategorySpreadTooWide\":false,\"flagIfNoCategoryContextAvailable\":false,\"minValue\":null,\"maxValue\":null},{\"key\":\"category_fit\",\"label\":\"Category Fit\",\"enabled\":false,\"sourceSupport\":\"context_only\",\"description\":\"Admin-Einschaetzung, ob die Person inhaltlich zur Unterkategorie passt.\",\"requiredForAutoClassification\":false,\"showInReview\":true,\"showInAdminSummary\":false,\"manualOverrideAllowed\":true,\"windowKey\":\"90d\",\"autoSupportedWindowKeys\":[],\"providerFieldKey\":null,\"topCount\":null,\"minPrimaryCategorySharePercent\":null,\"minPrimaryCategoryHours\":null,\"maxDistinctCategoriesBeforeFlag\":null,\"ignoredCategories\":[],\"matchAwardCategoryAgainstTopCategories\":false,\"flagIfAwardCategoryNotInTopX\":false,\"flagIfCategorySpreadTooWide\":false,\"flagIfNoCategoryContextAvailable\":false,\"minValue\":null,\"maxValue\":null},{\"key\":\"top_categories_context\",\"label\":\"Top Categories Context\",\"enabled\":false,\"sourceSupport\":\"context_only\",\"description\":\"Manueller Kontext aus zuletzt meistgestreamten Kategorien oder Games des Channels.\",\"requiredForAutoClassification\":false,\"showInReview\":true,\"showInAdminSummary\":true,\"manualOverrideAllowed\":true,\"windowKey\":\"90d\",\"autoSupportedWindowKeys\":[],\"providerFieldKey\":null,\"topCount\":5,\"minPrimaryCategorySharePercent\":60,\"minPrimaryCategoryHours\":20,\"maxDistinctCategoriesBeforeFlag\":6,\"ignoredCategories\":[\"Just Chatting\",\"Special Events\"],\"matchAwardCategoryAgainstTopCategories\":true,\"flagIfAwardCategoryNotInTopX\":true,\"flagIfCategorySpreadTooWide\":true,\"flagIfNoCategoryContextAvailable\":true,\"minValue\":null,\"maxValue\":null}],\"flags\":[{\"key\":\"tracker_unresolved\",\"label\":\"Tracker-Link nicht aufloesbar\",\"enabled\":true,\"severity\":\"high\",\"description\":\"Der Stream-Link konnte nicht sauber in einen Twitch-Channel aufgeloest werden.\",\"autoTriggerEnabled\":true,\"requiresManualReview\":true,\"blocksApproval\":false,\"adminNoteRequiredOnOverride\":false},{\"key\":\"unsupported_platform\",\"label\":\"Plattform nicht unterstuetzt\",\"enabled\":true,\"severity\":\"medium\",\"description\":\"Die Nominierung zeigt auf eine Plattform ohne automatische TwitchTracker-Daten.\",\"autoTriggerEnabled\":true,\"requiresManualReview\":true,\"blocksApproval\":false,\"adminNoteRequiredOnOverride\":false},{\"key\":\"no_tracker_data\",\"label\":\"Keine Tracker-Daten\",\"enabled\":true,\"severity\":\"medium\",\"description\":\"TwitchTracker hat keinen belastbaren Summary-Wert geliefert.\",\"autoTriggerEnabled\":true,\"requiresManualReview\":true,\"blocksApproval\":false,\"adminNoteRequiredOnOverride\":false},{\"key\":\"missing_required_metric\",\"label\":\"Pflichtmetrik fehlt\",\"enabled\":true,\"severity\":\"high\",\"description\":\"Mindestens eine aktivierte Pflichtmetrik fuer die Auto-Klassifizierung fehlt.\",\"autoTriggerEnabled\":true,\"requiresManualReview\":true,\"blocksApproval\":true,\"adminNoteRequiredOnOverride\":false},{\"key\":\"manual_review_required\",\"label\":\"Manuelle Pruefung noetig\",\"enabled\":true,\"severity\":\"medium\",\"description\":\"Die Auto-Daten reichen nicht fuer eine sichere Review-Entscheidung.\",\"autoTriggerEnabled\":true,\"requiresManualReview\":true,\"blocksApproval\":false,\"adminNoteRequiredOnOverride\":false},{\"key\":\"low_confidence_small_channel\",\"label\":\"Low Confidence Small Channel\",\"enabled\":false,\"severity\":\"low\",\"description\":\"Kleine Kanaele koennen manuell tiefer geprueft werden.\",\"autoTriggerEnabled\":false,\"requiresManualReview\":true,\"blocksApproval\":false,\"adminNoteRequiredOnOverride\":false},{\"key\":\"insufficient_activity_context\",\"label\":\"Zu wenig Aktivitaetskontext\",\"enabled\":false,\"severity\":\"low\",\"description\":\"Ohne zusaetzliche Kontextdaten wie Streamstunden ist die Bewertung unsicher.\",\"autoTriggerEnabled\":false,\"requiresManualReview\":true,\"blocksApproval\":false,\"adminNoteRequiredOnOverride\":false},{\"key\":\"category_fit_needs_review\",\"label\":\"Category Fit manuell pruefen\",\"enabled\":false,\"severity\":\"low\",\"description\":\"Unterkategorie muss inhaltlich manuell bestaetigt werden.\",\"autoTriggerEnabled\":false,\"requiresManualReview\":true,\"blocksApproval\":false,\"adminNoteRequiredOnOverride\":false},{\"key\":\"unsupported_metric_window\",\"label\":\"Gewaehltes Zeitfenster nicht auto-verfuegbar\",\"enabled\":true,\"severity\":\"medium\",\"description\":\"Die aktuelle TwitchTracker API liefert diese Metrik nicht fuer das konfigurierte Zeitfenster.\",\"autoTriggerEnabled\":true,\"requiresManualReview\":true,\"blocksApproval\":false,\"adminNoteRequiredOnOverride\":false}]}", + TwitchAuthManagedByDatabase = false, + TwitchClientId = "", + TwitchClientSecret = "", + TwitchRedirectUri = "", + TwitchScope = "", + ViewerStatsProviderBaseUrl = "https://twitchtracker.com/api", + WorkflowRulesJson = "[{\"key\":\"max_finalists_per_category\",\"label\":\"Finale Kandidat:innen pro Kategorie\",\"enabled\":true,\"limit\":4,\"mode\":\"block\",\"description\":\"Begrenzt, wie viele finale Kandidat:innen in einer Kategorie vorbereitet werden.\"},{\"key\":\"max_candidate_appearances\",\"label\":\"Kandidaturen pro Person\",\"enabled\":true,\"limit\":2,\"mode\":\"warn\",\"description\":\"Warnt oder blockiert, wenn dieselbe Person in zu vielen Kategorien final auftaucht.\"},{\"key\":\"max_winner_placements\",\"label\":\"Gewinnerpl\\u00E4tze pro Person\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn dieselbe Person mehrfach als Gewinner:in gesetzt wird.\"},{\"key\":\"winner_requires_clip\",\"label\":\"Gewinner braucht Clip-Link\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn ein Gewinner ohne gepflegten YouTube-/Twitch-Clip gesetzt wird.\"},{\"key\":\"recommended_nominators_per_subcategory\",\"label\":\"Empfohlene Nominierer pro Unterkategorie\",\"enabled\":true,\"limit\":4,\"mode\":\"warn\",\"description\":\"Zeigt in der Kategorie-\\u00DCbersicht an, ab wann eine Unterkategorie nominierungsseitig gut getragen ist. Diese Regel blockiert nichts.\"}]" + }); + }); + + modelBuilder.Entity("Backend.Domain.Sponsor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsVisible") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tier") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WebsiteUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "IsVisible", "SortOrder"); + + b.ToTable("Sponsors"); + }); + + modelBuilder.Entity("Backend.Domain.StreamerIdentity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("LastResolvedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Login") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("NormalizedKey") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("character varying(180)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ProfileUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedKey") + .IsUnique(); + + b.ToTable("StreamerIdentities"); + }); + + modelBuilder.Entity("Backend.Domain.TeamMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoundTwitchDisplayName") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("BoundTwitchUserId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Login") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("MustChangePassword") + .HasColumnType("boolean"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("PasswordResetAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("TwitchBoundAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByTwitchId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("BoundTwitchUserId") + .IsUnique(); + + b.HasIndex("Login") + .IsUnique(); + + b.ToTable("TeamMembers"); + }); + + modelBuilder.Entity("Backend.Domain.TeamRolePermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PermissionsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("Role") + .IsUnique(); + + b.ToTable("TeamRolePermissions"); + }); + + modelBuilder.Entity("Backend.Domain.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SessionToken") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("TwitchUserId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.HasKey("Id"); + + b.HasIndex("SessionToken") + .IsUnique(); + + b.ToTable("UserSessions"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubmittedByTwitchId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "SubmittedByTwitchId") + .IsUnique(); + + b.ToTable("VoteBallots"); + + b.HasData( + new + { + Id = 1, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_1" + }, + new + { + Id = 2, + SeasonId = 1, + Status = "submitted", + SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 5, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + SubmittedByTwitchId = "twitch_vote_2" + }); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BallotId") + .HasColumnType("integer"); + + b.Property("CandidateId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BallotId"); + + b.HasIndex("CandidateId"); + + b.HasIndex("CategoryId"); + + b.ToTable("VoteEntries"); + + b.HasData( + new + { + Id = 1, + BallotId = 1, + CandidateId = 1, + CategoryId = 1 + }, + new + { + Id = 2, + BallotId = 1, + CandidateId = 4, + CategoryId = 2 + }, + new + { + Id = 3, + BallotId = 2, + CandidateId = 2, + CategoryId = 1 + }, + new + { + Id = 4, + BallotId = 2, + CandidateId = 6, + CategoryId = 3 + }); + }); + + modelBuilder.Entity("Backend.Domain.AwardResult", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Results") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Candidate", b => + { + b.HasOne("Backend.Domain.Category", "Category") + .WithMany("Candidates") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.StreamerIdentity", "StreamerIdentity") + .WithMany("Candidates") + .HasForeignKey("StreamerIdentityId"); + + b.Navigation("Category"); + + b.Navigation("Season"); + + b.Navigation("StreamerIdentity"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany("Categories") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.ClipSubmission", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Candidate"); + }); + + modelBuilder.Entity("Backend.Domain.Nomination", b => + { + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId"); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.StreamerIdentity", "StreamerIdentity") + .WithMany("Nominations") + .HasForeignKey("StreamerIdentityId"); + + b.HasOne("Backend.Domain.Category", "SuggestedCategory") + .WithMany() + .HasForeignKey("SuggestedCategoryId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + + b.Navigation("Season"); + + b.Navigation("StreamerIdentity"); + + b.Navigation("SuggestedCategory"); + }); + + modelBuilder.Entity("Backend.Domain.RiskFlag", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId"); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.ShowactApplication", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.Sponsor", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.HasOne("Backend.Domain.Season", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("Backend.Domain.VoteEntry", b => + { + b.HasOne("Backend.Domain.VoteBallot", "Ballot") + .WithMany("Entries") + .HasForeignKey("BallotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Candidate", "Candidate") + .WithMany() + .HasForeignKey("CandidateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backend.Domain.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ballot"); + + b.Navigation("Candidate"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Backend.Domain.Category", b => + { + b.Navigation("Candidates"); + }); + + modelBuilder.Entity("Backend.Domain.Season", b => + { + b.Navigation("Categories"); + + b.Navigation("Results"); + }); + + modelBuilder.Entity("Backend.Domain.StreamerIdentity", b => + { + b.Navigation("Candidates"); + + b.Navigation("Nominations"); + }); + + modelBuilder.Entity("Backend.Domain.VoteBallot", b => + { + b.Navigation("Entries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/Migrations/20260628205353_AddSeasonWorkflowRulesJson.cs b/Backend/Migrations/20260628205353_AddSeasonWorkflowRulesJson.cs new file mode 100644 index 0000000..f1ebbd8 --- /dev/null +++ b/Backend/Migrations/20260628205353_AddSeasonWorkflowRulesJson.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backend.Migrations +{ + /// + public partial class AddSeasonWorkflowRulesJson : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "WorkflowRulesJson", + table: "Seasons", + type: "text", + nullable: false, + defaultValue: "[]"); + + migrationBuilder.Sql( + """ + UPDATE "Seasons" + SET "WorkflowRulesJson" = COALESCE(NULLIF((SELECT "WorkflowRulesJson" FROM "SiteSettings" WHERE "Id" = 1), ''), '[]') + WHERE COALESCE("WorkflowRulesJson", '') = ''; + """); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "WorkflowRulesJson", + table: "Seasons"); + } + } +} diff --git a/Backend/Migrations/20260629142009_InitialCleanBaseline.cs b/Backend/Migrations/20260629142009_InitialCleanBaseline.cs deleted file mode 100644 index ab484b5..0000000 --- a/Backend/Migrations/20260629142009_InitialCleanBaseline.cs +++ /dev/null @@ -1,795 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Backend.Migrations -{ - /// - public partial class InitialCleanBaseline : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "AdminAuditEntries", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - AdminTwitchUserId = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - ActionType = table.Column(type: "character varying(80)", maxLength: 80, nullable: false), - EntityType = table.Column(type: "character varying(80)", maxLength: 80, nullable: false), - EntityId = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - Summary = table.Column(type: "character varying(240)", maxLength: 240, nullable: false), - MetadataJson = table.Column(type: "text", nullable: false), - CreatedFromIp = table.Column(type: "character varying(80)", maxLength: 80, nullable: false), - UserAgent = table.Column(type: "character varying(400)", maxLength: 400, nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_AdminAuditEntries", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Seasons", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Year = table.Column(type: "integer", nullable: false), - Name = table.Column(type: "character varying(160)", maxLength: 160, nullable: false), - IsDemo = table.Column(type: "boolean", nullable: false, defaultValue: false), - IsCurrent = table.Column(type: "boolean", nullable: false), - IsCommunityOnly = table.Column(type: "boolean", nullable: false), - CurrentPhase = table.Column(type: "character varying(60)", maxLength: 60, nullable: false), - NominationStartsAt = table.Column(type: "date", nullable: false), - NominationEndsAt = table.Column(type: "date", nullable: false), - VotingStartsAt = table.Column(type: "date", nullable: false), - VotingEndsAt = table.Column(type: "date", nullable: false), - ReviewStartsAt = table.Column(type: "date", nullable: false), - ReviewEndsAt = table.Column(type: "date", nullable: false), - ShowDate = table.Column(type: "date", nullable: false), - ShowStartsAt = table.Column(type: "time without time zone", nullable: false), - WinnersPublishedAt = table.Column(type: "timestamp with time zone", nullable: true), - WinnersPublishedByTwitchId = table.Column(type: "character varying(120)", maxLength: 120, nullable: true), - SubcategoryTemplatesJson = table.Column(type: "text", nullable: false, defaultValue: "[]"), - WorkflowRulesJson = table.Column(type: "text", nullable: false, defaultValue: "[]") - }, - constraints: table => - { - table.PrimaryKey("PK_Seasons", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "SiteSettings", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - HostDisplayName = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - HostTagline = table.Column(type: "character varying(160)", maxLength: 160, nullable: false), - NewsletterUrl = table.Column(type: "character varying(400)", maxLength: 400, nullable: false), - ShareXUrl = table.Column(type: "text", nullable: false), - ShareDiscordUrl = table.Column(type: "text", nullable: false), - PrivacyEmail = table.Column(type: "character varying(160)", maxLength: 160, nullable: false), - PrivacyPolicyContent = table.Column(type: "text", nullable: false), - PrivacyPolicyUpdatedBy = table.Column(type: "character varying(120)", maxLength: 120, nullable: true), - PrivacyPolicyUpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - ImprintUrl = table.Column(type: "character varying(400)", maxLength: 400, nullable: false), - ImprintContent = table.Column(type: "text", nullable: false), - ContactUrl = table.Column(type: "character varying(400)", maxLength: 400, nullable: false), - ContactContent = table.Column(type: "text", nullable: false), - SponsorsUrl = table.Column(type: "character varying(400)", maxLength: 400, nullable: false), - SponsorsContent = table.Column(type: "text", nullable: false), - ShowactsUrl = table.Column(type: "character varying(400)", maxLength: 400, nullable: false), - ShowactsContent = table.Column(type: "text", nullable: false, defaultValue: ""), - StreamBannerEyebrow = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - StreamBannerTitle = table.Column(type: "character varying(160)", maxLength: 160, nullable: false), - StreamBannerText = table.Column(type: "text", nullable: false), - StreamBannerLiveButtonLabel = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - StreamBannerLiveButtonUrl = table.Column(type: "character varying(400)", maxLength: 400, nullable: false), - StreamBannerLockedButtonLabel = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - StreamBannerUseCompletedContent = table.Column(type: "boolean", nullable: false), - StreamBannerCompletedEyebrow = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - StreamBannerCompletedTitle = table.Column(type: "character varying(160)", maxLength: 160, nullable: false), - StreamBannerCompletedText = table.Column(type: "text", nullable: false), - StreamBannerCompletedButtonLabel = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - StreamBannerCompletedButtonUrl = table.Column(type: "character varying(400)", maxLength: 400, nullable: false), - AwardsSectionTitle = table.Column(type: "text", nullable: false), - AwardsSectionDescription = table.Column(type: "text", nullable: false), - SubcategoriesSectionTitle = table.Column(type: "text", nullable: false), - SubcategoriesSectionDescription = table.Column(type: "text", nullable: false), - SocialLinksJson = table.Column(type: "text", nullable: false), - FaqJson = table.Column(type: "text", nullable: false), - RiskRulesJson = table.Column(type: "text", nullable: false), - WorkflowRulesJson = table.Column(type: "text", nullable: false, defaultValue: "[]"), - TrackingRulesJson = table.Column(type: "text", nullable: false, defaultValue: "[]"), - ViewerStatsProviderBaseUrl = table.Column(type: "character varying(400)", maxLength: 400, nullable: false), - TrackingReviewNotes = table.Column(type: "text", nullable: false), - NominationLinkBlacklistJson = table.Column(type: "text", nullable: false, defaultValue: "[]"), - ClipSubmissionsEnabled = table.Column(type: "boolean", nullable: false, defaultValue: false), - ClipReviewEnabled = table.Column(type: "boolean", nullable: false, defaultValue: true), - ClipAdminMenuVisible = table.Column(type: "boolean", nullable: false), - ClipSubmissionDisabledMessage = table.Column(type: "character varying(240)", maxLength: 240, nullable: false), - ShowactApplicationsEnabled = table.Column(type: "boolean", nullable: false, defaultValue: false), - ShowactApplicationStartsAt = table.Column(type: "date", nullable: true), - ShowactApplicationEndsAt = table.Column(type: "date", nullable: true), - ShowactApplicationDisabledMessage = table.Column(type: "character varying(240)", maxLength: 240, nullable: false), - ShowactFormSchemaJson = table.Column(type: "text", nullable: false), - SponsorsVisible = table.Column(type: "boolean", nullable: false, defaultValue: true), - DemoLoginManagedByDatabase = table.Column(type: "boolean", nullable: false), - DemoLoginEnabled = table.Column(type: "boolean", nullable: false), - DemoLoginEmail = table.Column(type: "character varying(180)", maxLength: 180, nullable: false), - DemoLoginPasswordHash = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - DemoLoginPasswordSalt = table.Column(type: "character varying(80)", maxLength: 80, nullable: false), - DemoLoginTwitchUserId = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - DemoLoginDisplayName = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - TwitchAuthManagedByDatabase = table.Column(type: "boolean", nullable: false), - TwitchClientId = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - TwitchClientSecret = table.Column(type: "character varying(180)", maxLength: 180, nullable: false), - TwitchRedirectUri = table.Column(type: "character varying(400)", maxLength: 400, nullable: false), - TwitchScope = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), - SessionIdleTimeoutHours = table.Column(type: "integer", nullable: false, defaultValue: 3), - MaintenanceModeEnabled = table.Column(type: "boolean", nullable: false), - MaintenanceTitle = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - MaintenanceMessage = table.Column(type: "character varying(600)", maxLength: 600, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_SiteSettings", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "StreamerIdentities", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Platform = table.Column(type: "character varying(40)", maxLength: 40, nullable: false), - Login = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - NormalizedKey = table.Column(type: "character varying(180)", maxLength: 180, nullable: false), - DisplayName = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - ProfileUrl = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), - LastResolvedAt = table.Column(type: "timestamp with time zone", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_StreamerIdentities", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "TeamMembers", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Login = table.Column(type: "character varying(80)", maxLength: 80, nullable: false), - DisplayName = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - Role = table.Column(type: "character varying(40)", maxLength: 40, nullable: false), - PasswordHash = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - PasswordSalt = table.Column(type: "character varying(80)", maxLength: 80, nullable: false), - BoundTwitchUserId = table.Column(type: "character varying(120)", maxLength: 120, nullable: true), - BoundTwitchDisplayName = table.Column(type: "character varying(120)", maxLength: 120, nullable: true), - MustChangePassword = table.Column(type: "boolean", nullable: false), - IsActive = table.Column(type: "boolean", nullable: false), - CreatedByTwitchId = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - UpdatedByTwitchId = table.Column(type: "character varying(120)", maxLength: 120, nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - LastLoginAt = table.Column(type: "timestamp with time zone", nullable: true), - TwitchBoundAt = table.Column(type: "timestamp with time zone", nullable: true), - PasswordResetAt = table.Column(type: "timestamp with time zone", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_TeamMembers", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "TeamRolePermissions", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Role = table.Column(type: "character varying(40)", maxLength: 40, nullable: false), - PermissionsJson = table.Column(type: "text", nullable: false), - UpdatedByTwitchId = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_TeamRolePermissions", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "UserSessions", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - SessionToken = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - TwitchUserId = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - DisplayName = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - Role = table.Column(type: "character varying(40)", maxLength: 40, nullable: false), - CreatedFromIp = table.Column(type: "character varying(80)", maxLength: 80, nullable: false), - UserAgent = table.Column(type: "character varying(400)", maxLength: 400, nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - LastSeenAt = table.Column(type: "timestamp with time zone", nullable: false), - IsActive = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_UserSessions", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Categories", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - SeasonId = table.Column(type: "integer", nullable: false), - GroupName = table.Column(type: "character varying(80)", maxLength: 80, nullable: false), - Name = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - Slug = table.Column(type: "text", nullable: false), - Description = table.Column(type: "character varying(400)", maxLength: 400, nullable: false), - SortOrder = table.Column(type: "integer", nullable: false), - MaxNomineesPerUser = table.Column(type: "integer", nullable: false), - ViewerRangeMin = table.Column(type: "integer", nullable: true), - ViewerRangeMax = table.Column(type: "integer", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Categories", x => x.Id); - table.ForeignKey( - name: "FK_Categories_Seasons_SeasonId", - column: x => x.SeasonId, - principalTable: "Seasons", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "RiskFlags", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - SeasonId = table.Column(type: "integer", nullable: true), - TwitchUserId = table.Column(type: "character varying(120)", maxLength: 120, nullable: true), - Source = table.Column(type: "character varying(80)", maxLength: 80, nullable: false), - Type = table.Column(type: "character varying(80)", maxLength: 80, nullable: false), - Severity = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), - Status = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), - Summary = table.Column(type: "character varying(240)", maxLength: 240, nullable: false), - CreatedFromIp = table.Column(type: "character varying(80)", maxLength: 80, nullable: false), - UserAgent = table.Column(type: "character varying(400)", maxLength: 400, nullable: false), - MetadataJson = table.Column(type: "text", nullable: false), - ReviewNote = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), - ReviewedByTwitchId = table.Column(type: "character varying(120)", maxLength: 120, nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - ReviewedAt = table.Column(type: "timestamp with time zone", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_RiskFlags", x => x.Id); - table.ForeignKey( - name: "FK_RiskFlags_Seasons_SeasonId", - column: x => x.SeasonId, - principalTable: "Seasons", - principalColumn: "Id"); - }); - - migrationBuilder.CreateTable( - name: "ShowactApplications", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - SeasonId = table.Column(type: "integer", nullable: false), - ArtistName = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - ContactEmail = table.Column(type: "character varying(180)", maxLength: 180, nullable: false), - ContactDiscord = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - PlatformUrl = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), - PerformanceType = table.Column(type: "character varying(80)", maxLength: 80, nullable: false), - Description = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: false), - TechnicalNotes = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: false), - ReferenceUrl = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), - FieldResponsesJson = table.Column(type: "text", nullable: false), - Status = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), - ReviewNote = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), - ReviewedByTwitchId = table.Column(type: "character varying(120)", maxLength: 120, nullable: true), - CreatedFromIp = table.Column(type: "character varying(80)", maxLength: 80, nullable: false), - UserAgent = table.Column(type: "character varying(400)", maxLength: 400, nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - ReviewedAt = table.Column(type: "timestamp with time zone", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_ShowactApplications", x => x.Id); - table.ForeignKey( - name: "FK_ShowactApplications_Seasons_SeasonId", - column: x => x.SeasonId, - principalTable: "Seasons", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Sponsors", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - SeasonId = table.Column(type: "integer", nullable: false), - Name = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - WebsiteUrl = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), - LogoUrl = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), - Description = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), - Tier = table.Column(type: "character varying(80)", maxLength: 80, nullable: false), - SortOrder = table.Column(type: "integer", nullable: false), - IsVisible = table.Column(type: "boolean", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Sponsors", x => x.Id); - table.ForeignKey( - name: "FK_Sponsors_Seasons_SeasonId", - column: x => x.SeasonId, - principalTable: "Seasons", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "VoteBallots", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - SeasonId = table.Column(type: "integer", nullable: false), - SubmittedByTwitchId = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - Status = table.Column(type: "character varying(30)", maxLength: 30, nullable: false), - SubmittedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_VoteBallots", x => x.Id); - table.ForeignKey( - name: "FK_VoteBallots_Seasons_SeasonId", - column: x => x.SeasonId, - principalTable: "Seasons", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Candidates", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - SeasonId = table.Column(type: "integer", nullable: false), - CategoryId = table.Column(type: "integer", nullable: false), - StreamerIdentityId = table.Column(type: "integer", nullable: true), - DisplayName = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - ChannelSlug = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - Platform = table.Column(type: "character varying(40)", maxLength: 40, nullable: false), - NominationTally = table.Column(type: "integer", nullable: false, defaultValue: 0), - AcceptanceStatus = table.Column(type: "character varying(30)", maxLength: 30, nullable: false, defaultValue: "open"), - AcceptanceNote = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), - ClipCompilationUrl = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), - ClipCompilationTitle = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), - ClipCompilationPlatform = table.Column(type: "character varying(40)", maxLength: 40, nullable: true), - ClipEmbedStatus = table.Column(type: "character varying(30)", maxLength: 30, nullable: false, defaultValue: "unchecked") - }, - constraints: table => - { - table.PrimaryKey("PK_Candidates", x => x.Id); - table.ForeignKey( - name: "FK_Candidates_Categories_CategoryId", - column: x => x.CategoryId, - principalTable: "Categories", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Candidates_Seasons_SeasonId", - column: x => x.SeasonId, - principalTable: "Seasons", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Candidates_StreamerIdentities_StreamerIdentityId", - column: x => x.StreamerIdentityId, - principalTable: "StreamerIdentities", - principalColumn: "Id"); - }); - - migrationBuilder.CreateTable( - name: "ClipSubmissions", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - SeasonId = table.Column(type: "integer", nullable: false), - CategoryId = table.Column(type: "integer", nullable: true), - CandidateId = table.Column(type: "integer", nullable: true), - SubmittedByTwitchId = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - ClipUrl = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), - Title = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - Creator = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - Platform = table.Column(type: "character varying(40)", maxLength: 40, nullable: false), - Status = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), - ReviewNote = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), - ReviewedByTwitchId = table.Column(type: "character varying(120)", maxLength: 120, nullable: true), - CreatedFromIp = table.Column(type: "character varying(80)", maxLength: 80, nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - ReviewedAt = table.Column(type: "timestamp with time zone", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_ClipSubmissions", x => x.Id); - table.ForeignKey( - name: "FK_ClipSubmissions_Candidates_CandidateId", - column: x => x.CandidateId, - principalTable: "Candidates", - principalColumn: "Id", - onDelete: ReferentialAction.SetNull); - table.ForeignKey( - name: "FK_ClipSubmissions_Seasons_SeasonId", - column: x => x.SeasonId, - principalTable: "Seasons", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Nominations", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - SeasonId = table.Column(type: "integer", nullable: false), - CategoryId = table.Column(type: "integer", nullable: true), - CategoryGroupName = table.Column(type: "character varying(80)", maxLength: 80, nullable: false, defaultValue: ""), - SubmittedByTwitchId = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - CandidateId = table.Column(type: "integer", nullable: true), - StreamerIdentityId = table.Column(type: "integer", nullable: true), - SuggestedCategoryId = table.Column(type: "integer", nullable: true), - CandidateText = table.Column(type: "character varying(120)", maxLength: 120, nullable: true), - StreamUrl = table.Column(type: "character varying(300)", maxLength: 300, nullable: true), - ResolvedChannel = table.Column(type: "character varying(120)", maxLength: 120, nullable: true), - ResolvedPlatform = table.Column(type: "character varying(40)", maxLength: 40, nullable: true), - AvgViewers = table.Column(type: "integer", nullable: true), - HoursStreamed = table.Column(type: "integer", nullable: true), - HoursWatched = table.Column(type: "integer", nullable: true), - PeakViewers = table.Column(type: "integer", nullable: true), - FollowersGained = table.Column(type: "integer", nullable: true), - TrackerStatus = table.Column(type: "character varying(40)", maxLength: 40, nullable: false, defaultValue: "pending"), - TrackerCheckedAt = table.Column(type: "timestamp with time zone", nullable: true), - TrackingReviewStatus = table.Column(type: "character varying(30)", maxLength: 30, nullable: false, defaultValue: "clear"), - TrackingFlagsJson = table.Column(type: "text", nullable: false, defaultValue: "[]"), - TrackingReviewNote = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), - TrackingReviewedByTwitchId = table.Column(type: "character varying(120)", maxLength: 120, nullable: true), - TrackingReviewedAt = table.Column(type: "timestamp with time zone", nullable: true), - Status = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), - ReviewNote = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), - ReviewedByTwitchId = table.Column(type: "character varying(120)", maxLength: 120, nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - ReviewedAt = table.Column(type: "timestamp with time zone", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Nominations", x => x.Id); - table.ForeignKey( - name: "FK_Nominations_Candidates_CandidateId", - column: x => x.CandidateId, - principalTable: "Candidates", - principalColumn: "Id"); - table.ForeignKey( - name: "FK_Nominations_Categories_CategoryId", - column: x => x.CategoryId, - principalTable: "Categories", - principalColumn: "Id", - onDelete: ReferentialAction.SetNull); - table.ForeignKey( - name: "FK_Nominations_Categories_SuggestedCategoryId", - column: x => x.SuggestedCategoryId, - principalTable: "Categories", - principalColumn: "Id", - onDelete: ReferentialAction.SetNull); - table.ForeignKey( - name: "FK_Nominations_Seasons_SeasonId", - column: x => x.SeasonId, - principalTable: "Seasons", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Nominations_StreamerIdentities_StreamerIdentityId", - column: x => x.StreamerIdentityId, - principalTable: "StreamerIdentities", - principalColumn: "Id"); - }); - - migrationBuilder.CreateTable( - name: "Results", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - SeasonId = table.Column(type: "integer", nullable: false), - CategoryId = table.Column(type: "integer", nullable: false), - CandidateId = table.Column(type: "integer", nullable: false), - CategoryName = table.Column(type: "character varying(120)", maxLength: 120, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Results", x => x.Id); - table.ForeignKey( - name: "FK_Results_Candidates_CandidateId", - column: x => x.CandidateId, - principalTable: "Candidates", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Results_Categories_CategoryId", - column: x => x.CategoryId, - principalTable: "Categories", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Results_Seasons_SeasonId", - column: x => x.SeasonId, - principalTable: "Seasons", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "VoteEntries", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - BallotId = table.Column(type: "integer", nullable: false), - CategoryId = table.Column(type: "integer", nullable: false), - CandidateId = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_VoteEntries", x => x.Id); - table.ForeignKey( - name: "FK_VoteEntries_Candidates_CandidateId", - column: x => x.CandidateId, - principalTable: "Candidates", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_VoteEntries_Categories_CategoryId", - column: x => x.CategoryId, - principalTable: "Categories", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_VoteEntries_VoteBallots_BallotId", - column: x => x.BallotId, - principalTable: "VoteBallots", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_Candidates_CategoryId", - table: "Candidates", - column: "CategoryId"); - - migrationBuilder.CreateIndex( - name: "IX_Candidates_SeasonId", - table: "Candidates", - column: "SeasonId"); - - migrationBuilder.CreateIndex( - name: "IX_Candidates_StreamerIdentityId", - table: "Candidates", - column: "StreamerIdentityId"); - - migrationBuilder.CreateIndex( - name: "IX_Categories_SeasonId_Slug", - table: "Categories", - columns: new[] { "SeasonId", "Slug" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_ClipSubmissions_CandidateId", - table: "ClipSubmissions", - column: "CandidateId"); - - migrationBuilder.CreateIndex( - name: "IX_ClipSubmissions_SeasonId_Status", - table: "ClipSubmissions", - columns: new[] { "SeasonId", "Status" }); - - migrationBuilder.CreateIndex( - name: "IX_Nominations_CandidateId", - table: "Nominations", - column: "CandidateId"); - - migrationBuilder.CreateIndex( - name: "IX_Nominations_CategoryId", - table: "Nominations", - column: "CategoryId"); - - migrationBuilder.CreateIndex( - name: "IX_Nominations_SeasonId_CategoryGroupName_Status", - table: "Nominations", - columns: new[] { "SeasonId", "CategoryGroupName", "Status" }); - - migrationBuilder.CreateIndex( - name: "IX_Nominations_SeasonId_Status", - table: "Nominations", - columns: new[] { "SeasonId", "Status" }); - - migrationBuilder.CreateIndex( - name: "IX_Nominations_SeasonId_StreamerIdentityId_CategoryGroupName", - table: "Nominations", - columns: new[] { "SeasonId", "StreamerIdentityId", "CategoryGroupName" }); - - migrationBuilder.CreateIndex( - name: "IX_Nominations_StreamerIdentityId", - table: "Nominations", - column: "StreamerIdentityId"); - - migrationBuilder.CreateIndex( - name: "IX_Nominations_SuggestedCategoryId", - table: "Nominations", - column: "SuggestedCategoryId"); - - migrationBuilder.CreateIndex( - name: "IX_Results_CandidateId", - table: "Results", - column: "CandidateId"); - - migrationBuilder.CreateIndex( - name: "IX_Results_CategoryId", - table: "Results", - column: "CategoryId"); - - migrationBuilder.CreateIndex( - name: "IX_Results_SeasonId_CategoryId", - table: "Results", - columns: new[] { "SeasonId", "CategoryId" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_RiskFlags_SeasonId", - table: "RiskFlags", - column: "SeasonId"); - - migrationBuilder.CreateIndex( - name: "IX_Seasons_Year", - table: "Seasons", - column: "Year", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_ShowactApplications_SeasonId_Status", - table: "ShowactApplications", - columns: new[] { "SeasonId", "Status" }); - - migrationBuilder.CreateIndex( - name: "IX_Sponsors_SeasonId_IsVisible_SortOrder", - table: "Sponsors", - columns: new[] { "SeasonId", "IsVisible", "SortOrder" }); - - migrationBuilder.CreateIndex( - name: "IX_StreamerIdentities_NormalizedKey", - table: "StreamerIdentities", - column: "NormalizedKey", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_TeamMembers_BoundTwitchUserId", - table: "TeamMembers", - column: "BoundTwitchUserId", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_TeamMembers_Login", - table: "TeamMembers", - column: "Login", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_TeamRolePermissions_Role", - table: "TeamRolePermissions", - column: "Role", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_UserSessions_SessionToken", - table: "UserSessions", - column: "SessionToken", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_VoteBallots_SeasonId_SubmittedByTwitchId", - table: "VoteBallots", - columns: new[] { "SeasonId", "SubmittedByTwitchId" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_VoteEntries_BallotId", - table: "VoteEntries", - column: "BallotId"); - - migrationBuilder.CreateIndex( - name: "IX_VoteEntries_CandidateId", - table: "VoteEntries", - column: "CandidateId"); - - migrationBuilder.CreateIndex( - name: "IX_VoteEntries_CategoryId", - table: "VoteEntries", - column: "CategoryId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "AdminAuditEntries"); - - migrationBuilder.DropTable( - name: "ClipSubmissions"); - - migrationBuilder.DropTable( - name: "Nominations"); - - migrationBuilder.DropTable( - name: "Results"); - - migrationBuilder.DropTable( - name: "RiskFlags"); - - migrationBuilder.DropTable( - name: "ShowactApplications"); - - migrationBuilder.DropTable( - name: "SiteSettings"); - - migrationBuilder.DropTable( - name: "Sponsors"); - - migrationBuilder.DropTable( - name: "TeamMembers"); - - migrationBuilder.DropTable( - name: "TeamRolePermissions"); - - migrationBuilder.DropTable( - name: "UserSessions"); - - migrationBuilder.DropTable( - name: "VoteEntries"); - - migrationBuilder.DropTable( - name: "Candidates"); - - migrationBuilder.DropTable( - name: "VoteBallots"); - - migrationBuilder.DropTable( - name: "Categories"); - - migrationBuilder.DropTable( - name: "StreamerIdentities"); - - migrationBuilder.DropTable( - name: "Seasons"); - } - } -} diff --git a/Backend/Migrations/InitialCreate.manual.sql b/Backend/Migrations/InitialCreate.manual.sql new file mode 100644 index 0000000..9658792 --- /dev/null +++ b/Backend/Migrations/InitialCreate.manual.sql @@ -0,0 +1,159 @@ +CREATE TABLE "__EFMigrationsHistory" ( + "MigrationId" character varying(150) NOT NULL, + "ProductVersion" character varying(32) NOT NULL, + CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId") +); + +CREATE TABLE "Seasons" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "Year" integer NOT NULL, + "Name" character varying(160) NOT NULL, + "IsCurrent" boolean NOT NULL, + "IsCommunityOnly" boolean NOT NULL, + "CurrentPhase" character varying(60) NOT NULL, + "NominationStartsAt" date NOT NULL, + "NominationEndsAt" date NOT NULL, + "VotingStartsAt" date NOT NULL, + "VotingEndsAt" date NOT NULL, + "ReviewStartsAt" date NOT NULL, + "ReviewEndsAt" date NOT NULL, + "ShowDate" date NOT NULL +); + +CREATE UNIQUE INDEX "IX_Seasons_Year" ON "Seasons" ("Year"); + +CREATE TABLE "Categories" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "SeasonId" integer NOT NULL REFERENCES "Seasons" ("Id") ON DELETE CASCADE, + "GroupName" character varying(80) NOT NULL, + "Name" character varying(120) NOT NULL, + "Slug" text NOT NULL, + "Description" character varying(400) NOT NULL, + "SortOrder" integer NOT NULL, + "MaxNomineesPerUser" integer NOT NULL +); + +CREATE UNIQUE INDEX "IX_Categories_SeasonId_Slug" ON "Categories" ("SeasonId", "Slug"); + +CREATE TABLE "VoteBallots" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "SeasonId" integer NOT NULL REFERENCES "Seasons" ("Id") ON DELETE CASCADE, + "SubmittedByTwitchId" character varying(120) NOT NULL, + "Status" character varying(30) NOT NULL, + "SubmittedAt" timestamp with time zone NOT NULL +); + +CREATE INDEX "IX_VoteBallots_SeasonId" ON "VoteBallots" ("SeasonId"); + +CREATE TABLE "Candidates" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "SeasonId" integer NOT NULL REFERENCES "Seasons" ("Id") ON DELETE CASCADE, + "CategoryId" integer NOT NULL REFERENCES "Categories" ("Id") ON DELETE CASCADE, + "DisplayName" character varying(120) NOT NULL, + "ChannelSlug" character varying(120) NOT NULL, + "Platform" character varying(40) NOT NULL +); + +CREATE INDEX "IX_Candidates_CategoryId" ON "Candidates" ("CategoryId"); +CREATE INDEX "IX_Candidates_SeasonId" ON "Candidates" ("SeasonId"); + +CREATE TABLE "Nominations" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "SeasonId" integer NOT NULL REFERENCES "Seasons" ("Id") ON DELETE CASCADE, + "CategoryId" integer NOT NULL REFERENCES "Categories" ("Id") ON DELETE CASCADE, + "SubmittedByTwitchId" character varying(120) NOT NULL, + "CandidateId" integer NULL REFERENCES "Candidates" ("Id"), + "CandidateText" character varying(120) NULL, + "CreatedAt" timestamp with time zone NOT NULL +); + +CREATE INDEX "IX_Nominations_CandidateId" ON "Nominations" ("CandidateId"); +CREATE INDEX "IX_Nominations_CategoryId" ON "Nominations" ("CategoryId"); +CREATE INDEX "IX_Nominations_SeasonId" ON "Nominations" ("SeasonId"); + +CREATE TABLE "Results" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "SeasonId" integer NOT NULL REFERENCES "Seasons" ("Id") ON DELETE CASCADE, + "CandidateId" integer NOT NULL REFERENCES "Candidates" ("Id") ON DELETE CASCADE, + "CategoryName" character varying(120) NOT NULL +); + +CREATE INDEX "IX_Results_CandidateId" ON "Results" ("CandidateId"); +CREATE INDEX "IX_Results_SeasonId" ON "Results" ("SeasonId"); + +CREATE TABLE "VoteEntries" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "BallotId" integer NOT NULL REFERENCES "VoteBallots" ("Id") ON DELETE CASCADE, + "CategoryId" integer NOT NULL REFERENCES "Categories" ("Id") ON DELETE CASCADE, + "CandidateId" integer NOT NULL REFERENCES "Candidates" ("Id") ON DELETE CASCADE +); + +CREATE INDEX "IX_VoteEntries_BallotId" ON "VoteEntries" ("BallotId"); +CREATE INDEX "IX_VoteEntries_CandidateId" ON "VoteEntries" ("CandidateId"); +CREATE INDEX "IX_VoteEntries_CategoryId" ON "VoteEntries" ("CategoryId"); + +INSERT INTO "Seasons" ("Id", "CurrentPhase", "IsCommunityOnly", "IsCurrent", "Name", "NominationEndsAt", "NominationStartsAt", "ReviewEndsAt", "ReviewStartsAt", "ShowDate", "VotingEndsAt", "VotingStartsAt", "Year") VALUES +(1, 'Community Voting', true, true, 'VTuber Star Awards 2026', '2026-05-31', '2026-05-01', '2026-07-10', '2026-07-01', '2026-07-20', '2026-06-30', '2026-06-01', 2026), +(2, 'Archived', true, false, 'VTuber Star Awards 2025', '2025-05-31', '2025-05-01', '2025-07-10', '2025-07-01', '2025-07-20', '2025-06-30', '2025-06-01', 2025), +(3, 'Archived', true, false, 'VTuber Star Awards 2024', '2024-05-31', '2024-05-01', '2024-07-10', '2024-07-01', '2024-07-20', '2024-06-30', '2024-06-01', 2024), +(4, 'Archived', true, false, 'VTuber Star Awards 2023', '2023-05-31', '2023-05-01', '2023-07-10', '2023-07-01', '2023-07-20', '2023-06-30', '2023-06-01', 2023); + +INSERT INTO "Categories" ("Id", "Description", "GroupName", "MaxNomineesPerUser", "Name", "SeasonId", "Slug", "SortOrder") VALUES +(1, 'Die groesste Auszeichnung des Jahres.', 'Main Awards', 3, 'VTuber des Jahres', 1, 'vtuber-des-jahres', 1), +(2, 'Events, Konzerte und 3D-Shows.', 'Performance', 3, 'Bestes Live Event', 1, 'bestes-live-event', 2), +(3, 'Der lustigste oder emotionalste Clip des Jahres.', 'Clips & Highlights', 3, 'Clip des Jahres', 1, 'clip-des-jahres', 3), +(4, 'Die aktivste und freundlichste Community.', 'Main Awards', 3, 'Beste Community', 1, 'beste-community', 4), +(5, 'Archivkategorie 2025.', 'Main Awards', 3, 'VTuber des Jahres', 2, 'vtuber-des-jahres', 1), +(6, 'Archivkategorie 2025.', 'Performance', 3, 'Bestes Live Event', 2, 'bestes-live-event', 2), +(7, 'Archivkategorie 2025.', 'Clips & Highlights', 3, 'Clip des Jahres', 2, 'clip-des-jahres', 3), +(8, 'Archivkategorie 2024.', 'Main Awards', 3, 'VTuber des Jahres', 3, 'vtuber-des-jahres', 1), +(9, 'Archivkategorie 2024.', 'Clips & Highlights', 3, 'Clip des Jahres', 3, 'clip-des-jahres', 2), +(10, 'Archivkategorie 2023.', 'Main Awards', 3, 'VTuber des Jahres', 4, 'vtuber-des-jahres', 1); + +INSERT INTO "VoteBallots" ("Id", "SeasonId", "Status", "SubmittedAt", "SubmittedByTwitchId") VALUES +(1, 1, 'submitted', '2026-06-11T12:00:00+00:00', 'twitch_vote_1'), +(2, 1, 'submitted', '2026-06-11T12:05:00+00:00', 'twitch_vote_2'); + +INSERT INTO "Candidates" ("Id", "CategoryId", "ChannelSlug", "DisplayName", "Platform", "SeasonId") VALUES +(1, 1, '@hoshimimiyu', 'Hoshimi Miyu', 'Twitch', 1), +(2, 1, '@kurainu', 'Kurainu', 'Twitch', 1), +(3, 1, '@shiroch', 'Shiro Ch.', 'Twitch', 1), +(4, 2, '@kurainu', 'Kurainu 3D Live', 'Twitch', 1), +(5, 2, '@aoisakura', 'Aoi Sakura Showcase', 'YouTube', 1), +(6, 3, '@pyonkichikingdom', 'Pyonkichi Kingdom', 'Twitch', 1), +(7, 4, '@moonrelay', 'Moonrelay', 'Twitch', 1), +(8, 5, '@hoshimimiyu', 'Hoshimi Miyu', 'Twitch', 2), +(9, 6, '@kurainu', 'Kurainu 3D Live', 'Twitch', 2), +(10, 7, '@pyonkichikingdom', 'Pyonkichi Kingdom', 'Twitch', 2), +(11, 8, '@aoisakura', 'Aoi Sakura', 'YouTube', 3), +(12, 9, '@starbyte', 'Starbyte', 'Twitch', 3), +(13, 10, '@tenshivox', 'Tenshi Vox', 'Twitch', 4); + +INSERT INTO "Nominations" ("Id", "CandidateId", "CandidateText", "CategoryId", "CreatedAt", "SeasonId", "SubmittedByTwitchId") VALUES +(1, NULL, 'Hoshimi Miyu', 1, '2026-06-10T13:00:00+00:00', 1, 'twitch_hoshi'), +(2, NULL, 'Kurainu 3D Live', 2, '2026-06-10T14:00:00+00:00', 1, 'twitch_kurainu'); + +INSERT INTO "Results" ("Id", "CandidateId", "CategoryName", "SeasonId") VALUES +(1, 8, 'VTuber des Jahres', 2), +(2, 9, 'Bestes Live Event', 2), +(3, 10, 'Clip des Jahres', 2), +(4, 11, 'VTuber des Jahres', 3), +(5, 12, 'Clip des Jahres', 3), +(6, 13, 'VTuber des Jahres', 4); + +INSERT INTO "VoteEntries" ("Id", "BallotId", "CandidateId", "CategoryId") VALUES +(1, 1, 1, 1), +(2, 1, 4, 2), +(3, 2, 2, 1), +(4, 2, 6, 3); + +SELECT setval(pg_get_serial_sequence('"Seasons"', 'Id'), COALESCE(MAX("Id"), 1), true) FROM "Seasons"; +SELECT setval(pg_get_serial_sequence('"Categories"', 'Id'), COALESCE(MAX("Id"), 1), true) FROM "Categories"; +SELECT setval(pg_get_serial_sequence('"VoteBallots"', 'Id'), COALESCE(MAX("Id"), 1), true) FROM "VoteBallots"; +SELECT setval(pg_get_serial_sequence('"Candidates"', 'Id'), COALESCE(MAX("Id"), 1), true) FROM "Candidates"; +SELECT setval(pg_get_serial_sequence('"Nominations"', 'Id'), COALESCE(MAX("Id"), 1), true) FROM "Nominations"; +SELECT setval(pg_get_serial_sequence('"Results"', 'Id'), COALESCE(MAX("Id"), 1), true) FROM "Results"; +SELECT setval(pg_get_serial_sequence('"VoteEntries"', 'Id'), COALESCE(MAX("Id"), 1), true) FROM "VoteEntries"; + +INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") +VALUES ('20260617060000_InitialCreate', '8.0.11'); diff --git a/Backend/Migrations/InitialCreate.sql b/Backend/Migrations/InitialCreate.sql new file mode 100644 index 0000000..28c3474 --- /dev/null +++ b/Backend/Migrations/InitialCreate.sql @@ -0,0 +1,257 @@ +CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" ( + "MigrationId" character varying(150) NOT NULL, + "ProductVersion" character varying(32) NOT NULL, + CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId") +); + +START TRANSACTION; + +CREATE TABLE "Seasons" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY, + "Year" integer NOT NULL, + "Name" character varying(160) NOT NULL, + "IsCurrent" boolean NOT NULL, + "IsCommunityOnly" boolean NOT NULL, + "CurrentPhase" character varying(60) NOT NULL, + "NominationStartsAt" date NOT NULL, + "NominationEndsAt" date NOT NULL, + "VotingStartsAt" date NOT NULL, + "VotingEndsAt" date NOT NULL, + "ReviewStartsAt" date NOT NULL, + "ReviewEndsAt" date NOT NULL, + "ShowDate" date NOT NULL, + CONSTRAINT "PK_Seasons" PRIMARY KEY ("Id") +); + +CREATE TABLE "Categories" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY, + "SeasonId" integer NOT NULL, + "GroupName" character varying(80) NOT NULL, + "Name" character varying(120) NOT NULL, + "Slug" text NOT NULL, + "Description" character varying(400) NOT NULL, + "SortOrder" integer NOT NULL, + "MaxNomineesPerUser" integer NOT NULL, + CONSTRAINT "PK_Categories" PRIMARY KEY ("Id"), + CONSTRAINT "FK_Categories_Seasons_SeasonId" FOREIGN KEY ("SeasonId") REFERENCES "Seasons" ("Id") ON DELETE CASCADE +); + +CREATE TABLE "VoteBallots" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY, + "SeasonId" integer NOT NULL, + "SubmittedByTwitchId" character varying(120) NOT NULL, + "Status" character varying(30) NOT NULL, + "SubmittedAt" timestamp with time zone NOT NULL, + CONSTRAINT "PK_VoteBallots" PRIMARY KEY ("Id"), + CONSTRAINT "FK_VoteBallots_Seasons_SeasonId" FOREIGN KEY ("SeasonId") REFERENCES "Seasons" ("Id") ON DELETE CASCADE +); + +CREATE TABLE "Candidates" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY, + "SeasonId" integer NOT NULL, + "CategoryId" integer NOT NULL, + "DisplayName" character varying(120) NOT NULL, + "ChannelSlug" character varying(120) NOT NULL, + "Platform" character varying(40) NOT NULL, + CONSTRAINT "PK_Candidates" PRIMARY KEY ("Id"), + CONSTRAINT "FK_Candidates_Categories_CategoryId" FOREIGN KEY ("CategoryId") REFERENCES "Categories" ("Id") ON DELETE CASCADE, + CONSTRAINT "FK_Candidates_Seasons_SeasonId" FOREIGN KEY ("SeasonId") REFERENCES "Seasons" ("Id") ON DELETE CASCADE +); + +CREATE TABLE "Nominations" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY, + "SeasonId" integer NOT NULL, + "CategoryId" integer NOT NULL, + "SubmittedByTwitchId" character varying(120) NOT NULL, + "CandidateId" integer, + "CandidateText" character varying(120), + "CreatedAt" timestamp with time zone NOT NULL, + CONSTRAINT "PK_Nominations" PRIMARY KEY ("Id"), + CONSTRAINT "FK_Nominations_Candidates_CandidateId" FOREIGN KEY ("CandidateId") REFERENCES "Candidates" ("Id"), + CONSTRAINT "FK_Nominations_Categories_CategoryId" FOREIGN KEY ("CategoryId") REFERENCES "Categories" ("Id") ON DELETE CASCADE, + CONSTRAINT "FK_Nominations_Seasons_SeasonId" FOREIGN KEY ("SeasonId") REFERENCES "Seasons" ("Id") ON DELETE CASCADE +); + +CREATE TABLE "Results" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY, + "SeasonId" integer NOT NULL, + "CandidateId" integer NOT NULL, + "CategoryName" character varying(120) NOT NULL, + CONSTRAINT "PK_Results" PRIMARY KEY ("Id"), + CONSTRAINT "FK_Results_Candidates_CandidateId" FOREIGN KEY ("CandidateId") REFERENCES "Candidates" ("Id") ON DELETE CASCADE, + CONSTRAINT "FK_Results_Seasons_SeasonId" FOREIGN KEY ("SeasonId") REFERENCES "Seasons" ("Id") ON DELETE CASCADE +); + +CREATE TABLE "VoteEntries" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY, + "BallotId" integer NOT NULL, + "CategoryId" integer NOT NULL, + "CandidateId" integer NOT NULL, + CONSTRAINT "PK_VoteEntries" PRIMARY KEY ("Id"), + CONSTRAINT "FK_VoteEntries_Candidates_CandidateId" FOREIGN KEY ("CandidateId") REFERENCES "Candidates" ("Id") ON DELETE CASCADE, + CONSTRAINT "FK_VoteEntries_Categories_CategoryId" FOREIGN KEY ("CategoryId") REFERENCES "Categories" ("Id") ON DELETE CASCADE, + CONSTRAINT "FK_VoteEntries_VoteBallots_BallotId" FOREIGN KEY ("BallotId") REFERENCES "VoteBallots" ("Id") ON DELETE CASCADE +); + +INSERT INTO "Seasons" ("Id", "CurrentPhase", "IsCommunityOnly", "IsCurrent", "Name", "NominationEndsAt", "NominationStartsAt", "ReviewEndsAt", "ReviewStartsAt", "ShowDate", "VotingEndsAt", "VotingStartsAt", "Year") +VALUES (1, 'Community Voting', TRUE, TRUE, 'VTuber Star Awards 2026', DATE '2026-05-31', DATE '2026-05-01', DATE '2026-07-10', DATE '2026-07-01', DATE '2026-07-20', DATE '2026-06-30', DATE '2026-06-01', 2026); +INSERT INTO "Seasons" ("Id", "CurrentPhase", "IsCommunityOnly", "IsCurrent", "Name", "NominationEndsAt", "NominationStartsAt", "ReviewEndsAt", "ReviewStartsAt", "ShowDate", "VotingEndsAt", "VotingStartsAt", "Year") +VALUES (2, 'Archived', TRUE, FALSE, 'VTuber Star Awards 2025', DATE '2025-05-31', DATE '2025-05-01', DATE '2025-07-10', DATE '2025-07-01', DATE '2025-07-20', DATE '2025-06-30', DATE '2025-06-01', 2025); +INSERT INTO "Seasons" ("Id", "CurrentPhase", "IsCommunityOnly", "IsCurrent", "Name", "NominationEndsAt", "NominationStartsAt", "ReviewEndsAt", "ReviewStartsAt", "ShowDate", "VotingEndsAt", "VotingStartsAt", "Year") +VALUES (3, 'Archived', TRUE, FALSE, 'VTuber Star Awards 2024', DATE '2024-05-31', DATE '2024-05-01', DATE '2024-07-10', DATE '2024-07-01', DATE '2024-07-20', DATE '2024-06-30', DATE '2024-06-01', 2024); +INSERT INTO "Seasons" ("Id", "CurrentPhase", "IsCommunityOnly", "IsCurrent", "Name", "NominationEndsAt", "NominationStartsAt", "ReviewEndsAt", "ReviewStartsAt", "ShowDate", "VotingEndsAt", "VotingStartsAt", "Year") +VALUES (4, 'Archived', TRUE, FALSE, 'VTuber Star Awards 2023', DATE '2023-05-31', DATE '2023-05-01', DATE '2023-07-10', DATE '2023-07-01', DATE '2023-07-20', DATE '2023-06-30', DATE '2023-06-01', 2023); + +INSERT INTO "Categories" ("Id", "Description", "GroupName", "MaxNomineesPerUser", "Name", "SeasonId", "Slug", "SortOrder") +VALUES (1, 'Die groesste Auszeichnung des Jahres.', 'Main Awards', 3, 'VTuber des Jahres', 1, 'vtuber-des-jahres', 1); +INSERT INTO "Categories" ("Id", "Description", "GroupName", "MaxNomineesPerUser", "Name", "SeasonId", "Slug", "SortOrder") +VALUES (2, 'Events, Konzerte und 3D-Shows.', 'Performance', 3, 'Bestes Live Event', 1, 'bestes-live-event', 2); +INSERT INTO "Categories" ("Id", "Description", "GroupName", "MaxNomineesPerUser", "Name", "SeasonId", "Slug", "SortOrder") +VALUES (3, 'Der lustigste oder emotionalste Clip des Jahres.', 'Clips & Highlights', 3, 'Clip des Jahres', 1, 'clip-des-jahres', 3); +INSERT INTO "Categories" ("Id", "Description", "GroupName", "MaxNomineesPerUser", "Name", "SeasonId", "Slug", "SortOrder") +VALUES (4, 'Die aktivste und freundlichste Community.', 'Main Awards', 3, 'Beste Community', 1, 'beste-community', 4); +INSERT INTO "Categories" ("Id", "Description", "GroupName", "MaxNomineesPerUser", "Name", "SeasonId", "Slug", "SortOrder") +VALUES (5, 'Archivkategorie 2025.', 'Main Awards', 3, 'VTuber des Jahres', 2, 'vtuber-des-jahres', 1); +INSERT INTO "Categories" ("Id", "Description", "GroupName", "MaxNomineesPerUser", "Name", "SeasonId", "Slug", "SortOrder") +VALUES (6, 'Archivkategorie 2025.', 'Performance', 3, 'Bestes Live Event', 2, 'bestes-live-event', 2); +INSERT INTO "Categories" ("Id", "Description", "GroupName", "MaxNomineesPerUser", "Name", "SeasonId", "Slug", "SortOrder") +VALUES (7, 'Archivkategorie 2025.', 'Clips & Highlights', 3, 'Clip des Jahres', 2, 'clip-des-jahres', 3); +INSERT INTO "Categories" ("Id", "Description", "GroupName", "MaxNomineesPerUser", "Name", "SeasonId", "Slug", "SortOrder") +VALUES (8, 'Archivkategorie 2024.', 'Main Awards', 3, 'VTuber des Jahres', 3, 'vtuber-des-jahres', 1); +INSERT INTO "Categories" ("Id", "Description", "GroupName", "MaxNomineesPerUser", "Name", "SeasonId", "Slug", "SortOrder") +VALUES (9, 'Archivkategorie 2024.', 'Clips & Highlights', 3, 'Clip des Jahres', 3, 'clip-des-jahres', 2); +INSERT INTO "Categories" ("Id", "Description", "GroupName", "MaxNomineesPerUser", "Name", "SeasonId", "Slug", "SortOrder") +VALUES (10, 'Archivkategorie 2023.', 'Main Awards', 3, 'VTuber des Jahres', 4, 'vtuber-des-jahres', 1); + +INSERT INTO "VoteBallots" ("Id", "SeasonId", "Status", "SubmittedAt", "SubmittedByTwitchId") +VALUES (1, 1, 'submitted', TIMESTAMPTZ '2026-06-11T12:00:00+00:00', 'twitch_vote_1'); +INSERT INTO "VoteBallots" ("Id", "SeasonId", "Status", "SubmittedAt", "SubmittedByTwitchId") +VALUES (2, 1, 'submitted', TIMESTAMPTZ '2026-06-11T12:05:00+00:00', 'twitch_vote_2'); + +INSERT INTO "Candidates" ("Id", "CategoryId", "ChannelSlug", "DisplayName", "Platform", "SeasonId") +VALUES (1, 1, '@hoshimimiyu', 'Hoshimi Miyu', 'Twitch', 1); +INSERT INTO "Candidates" ("Id", "CategoryId", "ChannelSlug", "DisplayName", "Platform", "SeasonId") +VALUES (2, 1, '@kurainu', 'Kurainu', 'Twitch', 1); +INSERT INTO "Candidates" ("Id", "CategoryId", "ChannelSlug", "DisplayName", "Platform", "SeasonId") +VALUES (3, 1, '@shiroch', 'Shiro Ch.', 'Twitch', 1); +INSERT INTO "Candidates" ("Id", "CategoryId", "ChannelSlug", "DisplayName", "Platform", "SeasonId") +VALUES (4, 2, '@kurainu', 'Kurainu 3D Live', 'Twitch', 1); +INSERT INTO "Candidates" ("Id", "CategoryId", "ChannelSlug", "DisplayName", "Platform", "SeasonId") +VALUES (5, 2, '@aoisakura', 'Aoi Sakura Showcase', 'YouTube', 1); +INSERT INTO "Candidates" ("Id", "CategoryId", "ChannelSlug", "DisplayName", "Platform", "SeasonId") +VALUES (6, 3, '@pyonkichikingdom', 'Pyonkichi Kingdom', 'Twitch', 1); +INSERT INTO "Candidates" ("Id", "CategoryId", "ChannelSlug", "DisplayName", "Platform", "SeasonId") +VALUES (7, 4, '@moonrelay', 'Moonrelay', 'Twitch', 1); +INSERT INTO "Candidates" ("Id", "CategoryId", "ChannelSlug", "DisplayName", "Platform", "SeasonId") +VALUES (8, 5, '@hoshimimiyu', 'Hoshimi Miyu', 'Twitch', 2); +INSERT INTO "Candidates" ("Id", "CategoryId", "ChannelSlug", "DisplayName", "Platform", "SeasonId") +VALUES (9, 6, '@kurainu', 'Kurainu 3D Live', 'Twitch', 2); +INSERT INTO "Candidates" ("Id", "CategoryId", "ChannelSlug", "DisplayName", "Platform", "SeasonId") +VALUES (10, 7, '@pyonkichikingdom', 'Pyonkichi Kingdom', 'Twitch', 2); +INSERT INTO "Candidates" ("Id", "CategoryId", "ChannelSlug", "DisplayName", "Platform", "SeasonId") +VALUES (11, 8, '@aoisakura', 'Aoi Sakura', 'YouTube', 3); +INSERT INTO "Candidates" ("Id", "CategoryId", "ChannelSlug", "DisplayName", "Platform", "SeasonId") +VALUES (12, 9, '@starbyte', 'Starbyte', 'Twitch', 3); +INSERT INTO "Candidates" ("Id", "CategoryId", "ChannelSlug", "DisplayName", "Platform", "SeasonId") +VALUES (13, 10, '@tenshivox', 'Tenshi Vox', 'Twitch', 4); + +INSERT INTO "Nominations" ("Id", "CandidateId", "CandidateText", "CategoryId", "CreatedAt", "SeasonId", "SubmittedByTwitchId") +VALUES (1, NULL, 'Hoshimi Miyu', 1, TIMESTAMPTZ '2026-06-10T13:00:00+00:00', 1, 'twitch_hoshi'); +INSERT INTO "Nominations" ("Id", "CandidateId", "CandidateText", "CategoryId", "CreatedAt", "SeasonId", "SubmittedByTwitchId") +VALUES (2, NULL, 'Kurainu 3D Live', 2, TIMESTAMPTZ '2026-06-10T14:00:00+00:00', 1, 'twitch_kurainu'); + +INSERT INTO "Results" ("Id", "CandidateId", "CategoryName", "SeasonId") +VALUES (1, 8, 'VTuber des Jahres', 2); +INSERT INTO "Results" ("Id", "CandidateId", "CategoryName", "SeasonId") +VALUES (2, 9, 'Bestes Live Event', 2); +INSERT INTO "Results" ("Id", "CandidateId", "CategoryName", "SeasonId") +VALUES (3, 10, 'Clip des Jahres', 2); +INSERT INTO "Results" ("Id", "CandidateId", "CategoryName", "SeasonId") +VALUES (4, 11, 'VTuber des Jahres', 3); +INSERT INTO "Results" ("Id", "CandidateId", "CategoryName", "SeasonId") +VALUES (5, 12, 'Clip des Jahres', 3); +INSERT INTO "Results" ("Id", "CandidateId", "CategoryName", "SeasonId") +VALUES (6, 13, 'VTuber des Jahres', 4); + +INSERT INTO "VoteEntries" ("Id", "BallotId", "CandidateId", "CategoryId") +VALUES (1, 1, 1, 1); +INSERT INTO "VoteEntries" ("Id", "BallotId", "CandidateId", "CategoryId") +VALUES (2, 1, 4, 2); +INSERT INTO "VoteEntries" ("Id", "BallotId", "CandidateId", "CategoryId") +VALUES (3, 2, 2, 1); +INSERT INTO "VoteEntries" ("Id", "BallotId", "CandidateId", "CategoryId") +VALUES (4, 2, 6, 3); + +CREATE INDEX "IX_Candidates_CategoryId" ON "Candidates" ("CategoryId"); + +CREATE INDEX "IX_Candidates_SeasonId" ON "Candidates" ("SeasonId"); + +CREATE UNIQUE INDEX "IX_Categories_SeasonId_Slug" ON "Categories" ("SeasonId", "Slug"); + +CREATE INDEX "IX_Nominations_CandidateId" ON "Nominations" ("CandidateId"); + +CREATE INDEX "IX_Nominations_CategoryId" ON "Nominations" ("CategoryId"); + +CREATE INDEX "IX_Nominations_SeasonId" ON "Nominations" ("SeasonId"); + +CREATE INDEX "IX_Results_CandidateId" ON "Results" ("CandidateId"); + +CREATE INDEX "IX_Results_SeasonId" ON "Results" ("SeasonId"); + +CREATE UNIQUE INDEX "IX_Seasons_Year" ON "Seasons" ("Year"); + +CREATE INDEX "IX_VoteBallots_SeasonId" ON "VoteBallots" ("SeasonId"); + +CREATE INDEX "IX_VoteEntries_BallotId" ON "VoteEntries" ("BallotId"); + +CREATE INDEX "IX_VoteEntries_CandidateId" ON "VoteEntries" ("CandidateId"); + +CREATE INDEX "IX_VoteEntries_CategoryId" ON "VoteEntries" ("CategoryId"); + +SELECT setval( + pg_get_serial_sequence('"Seasons"', 'Id'), + GREATEST( + (SELECT MAX("Id") FROM "Seasons") + 1, + nextval(pg_get_serial_sequence('"Seasons"', 'Id'))), + false); +SELECT setval( + pg_get_serial_sequence('"Categories"', 'Id'), + GREATEST( + (SELECT MAX("Id") FROM "Categories") + 1, + nextval(pg_get_serial_sequence('"Categories"', 'Id'))), + false); +SELECT setval( + pg_get_serial_sequence('"VoteBallots"', 'Id'), + GREATEST( + (SELECT MAX("Id") FROM "VoteBallots") + 1, + nextval(pg_get_serial_sequence('"VoteBallots"', 'Id'))), + false); +SELECT setval( + pg_get_serial_sequence('"Candidates"', 'Id'), + GREATEST( + (SELECT MAX("Id") FROM "Candidates") + 1, + nextval(pg_get_serial_sequence('"Candidates"', 'Id'))), + false); +SELECT setval( + pg_get_serial_sequence('"Nominations"', 'Id'), + GREATEST( + (SELECT MAX("Id") FROM "Nominations") + 1, + nextval(pg_get_serial_sequence('"Nominations"', 'Id'))), + false); +SELECT setval( + pg_get_serial_sequence('"Results"', 'Id'), + GREATEST( + (SELECT MAX("Id") FROM "Results") + 1, + nextval(pg_get_serial_sequence('"Results"', 'Id'))), + false); +SELECT setval( + pg_get_serial_sequence('"VoteEntries"', 'Id'), + GREATEST( + (SELECT MAX("Id") FROM "VoteEntries") + 1, + nextval(pg_get_serial_sequence('"VoteEntries"', 'Id'))), + false); + +INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") +VALUES ('20260617060000_InitialCreate', '8.0.11'); + +COMMIT;