diff --git a/Backend/Endpoints/AdminSeasonDetailEndpoints.cs b/Backend/Endpoints/AdminSeasonDetailEndpoints.cs index 0388b41..a8de24a 100644 --- a/Backend/Endpoints/AdminSeasonDetailEndpoints.cs +++ b/Backend/Endpoints/AdminSeasonDetailEndpoints.cs @@ -76,21 +76,7 @@ public static partial class AdminSeasonManagementEndpoints ViewerRangeMin = category.ViewerRangeMin, ViewerRangeMax = category.ViewerRangeMax, })); - var visibleCategoryRows = categoryRows - .Where(category => SeasonSubcategoryTemplateSettings.MatchesTemplate( - new Backend.Domain.Category - { - GroupName = category.GroupName, - Name = category.Name, - Slug = category.Slug, - SortOrder = category.SortOrder, - ViewerRangeMin = category.ViewerRangeMin, - ViewerRangeMax = category.ViewerRangeMax, - }, - subcategoryTemplateSettings)) - .ToArray(); - - var categories = visibleCategoryRows + var categories = categoryRows .Select(category => new AdminCategoryItemDto( category.Id, category.GroupName, diff --git a/Backend/Migrations/20260629183000_SeedLiveDemoAwardData.cs b/Backend/Migrations/20260629183000_SeedLiveDemoAwardData.cs index 8d7d017..9a7bc2e 100644 --- a/Backend/Migrations/20260629183000_SeedLiveDemoAwardData.cs +++ b/Backend/Migrations/20260629183000_SeedLiveDemoAwardData.cs @@ -128,7 +128,7 @@ public partial class SeedLiveDemoAwardData : Migration award_groups.group_name, tiers.tier_name, award_groups.group_slug || '-' || tiers.tier_slug, - award_groups.description || ' Demo-Tier: ' || tiers.tier_name || '.', + award_groups.description, (award_groups.ord * 100) + (tiers.ord * 10), 3, tiers.min_viewers, diff --git a/Backend/Migrations/20260629185000_SeedCurrentSeasonDemoVotingData.cs b/Backend/Migrations/20260629185000_SeedCurrentSeasonDemoVotingData.cs index 965610c..2f391b2 100644 --- a/Backend/Migrations/20260629185000_SeedCurrentSeasonDemoVotingData.cs +++ b/Backend/Migrations/20260629185000_SeedCurrentSeasonDemoVotingData.cs @@ -45,7 +45,7 @@ public partial class SeedCurrentSeasonDemoVotingData : Migration WITH award_groups(ord, group_name, group_slug, description, name_prefix) AS ( VALUES - (1, 'Main Awards', 'main-awards', 'Die groessten Allround-Auszeichnungen der aktuellen Demo-Season.', 'Astra'), + (1, 'Main Awards', 'main-awards', 'Die groessten Allround-Auszeichnungen der aktuellen Season.', 'Astra'), (2, 'Discovery', 'discovery', 'Neue Stimmen, Debuets und Creator, die in dieser Season besonders sichtbar geworden sind.', 'Nova'), (3, 'Creative', 'creative', 'Model, Design, Rigging, Art und visuelle Praesentation im Stream.', 'Velvet'), (4, 'Performance', 'performance', 'Gesang, Musik, Buehnenpraesenz und besondere Live-Momente.', 'Melo'), @@ -76,7 +76,7 @@ public partial class SeedCurrentSeasonDemoVotingData : Migration award_groups.group_name, tiers.tier_name, award_groups.group_slug || '-' || tiers.tier_slug, - award_groups.description || ' Demo-Tier: ' || tiers.tier_name || '.', + award_groups.description, (award_groups.ord * 100) + (tiers.ord * 10), 3, tiers.min_viewers, diff --git a/Backend/Migrations/20260630061341_SanitizeDemoCategoryDescriptions.Designer.cs b/Backend/Migrations/20260630061341_SanitizeDemoCategoryDescriptions.Designer.cs new file mode 100644 index 0000000..92252d4 --- /dev/null +++ b/Backend/Migrations/20260630061341_SanitizeDemoCategoryDescriptions.Designer.cs @@ -0,0 +1,1620 @@ +// +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("20260630061341_SanitizeDemoCategoryDescriptions")] + partial class SanitizeDemoCategoryDescriptions + { + /// + 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.ArchivedWinner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Category") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Subcategory") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WinnerName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("WinnerUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Year", "Category", "Subcategory") + .IsUnique(); + + b.ToTable("ArchivedWinners"); + }); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + 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("IsDemo") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + 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("SubcategoryTemplatesJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("[]"); + + b.Property("VotingEndsAt") + .HasColumnType("date"); + + 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"); + + b.HasKey("Id"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("Seasons"); + }); + + 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("AwardsSectionDescription") + .IsRequired() + .HasColumnType("text"); + + b.Property("AwardsSectionTitle") + .IsRequired() + .HasColumnType("text"); + + 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("HostArtistName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("HostDisplayName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("HostImageContentType") + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("HostImageData") + .HasColumnType("bytea"); + + b.Property("HostImageUpdatedAt") + .HasColumnType("timestamp with time zone"); + + 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("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"); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + 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.HasOne("Backend.Domain.Season", null) + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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/20260630061341_SanitizeDemoCategoryDescriptions.cs b/Backend/Migrations/20260630061341_SanitizeDemoCategoryDescriptions.cs new file mode 100644 index 0000000..595235f --- /dev/null +++ b/Backend/Migrations/20260630061341_SanitizeDemoCategoryDescriptions.cs @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backend.Migrations +{ + /// + public partial class SanitizeDemoCategoryDescriptions : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql( + """ + UPDATE "Categories" + SET "Description" = trim( + regexp_replace( + replace("Description", 'aktuellen Demo-Season', 'aktuellen Season'), + '\s*Demo-Tier:\s*[^.]+\.', + '', + 'g' + ) + ) + WHERE "Description" LIKE '%Demo-Tier:%' + OR "Description" LIKE '%aktuellen Demo-Season%'; + """ + ); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql( + """ + UPDATE "Categories" + SET "Description" = "Description" || ' Demo-Tier: ' || "Name" || '.' + WHERE "ViewerRangeMin" IS NOT NULL + AND "Description" NOT LIKE '%Demo-Tier:%'; + + UPDATE "Categories" + SET "Description" = replace("Description", 'aktuellen Season', 'aktuellen Demo-Season') + WHERE "Slug" LIKE 'main-awards-%' + AND "Description" LIKE '%aktuellen Season%'; + """ + ); + } + } +} diff --git a/frontend/src/components/admin/useAdminCandidateManager.ts b/frontend/src/components/admin/useAdminCandidateManager.ts index e61f7ce..e20ceb4 100644 --- a/frontend/src/components/admin/useAdminCandidateManager.ts +++ b/frontend/src/components/admin/useAdminCandidateManager.ts @@ -86,6 +86,15 @@ export function useAdminCandidateManager() { const categoryOptions = computed(() => seasonDetail.value.categories.map((category) => ({ label: `${category.groupName} · ${category.name}`, value: category.id })), ) + const preferredCreateCategoryId = computed(() => { + if (categoryFilter.value) { + return categoryFilter.value + } + + return seasonDetail.value.categories.find((category) => + category.viewerRangeMin !== null || category.viewerRangeMax !== null, + )?.id ?? seasonDetail.value.categories[0]?.id ?? 0 + }) const categoryFilterOptions = computed(() => [{ label: 'Alle Kategorien', value: null }, ...categoryOptions.value]) const categoryLabelMap = computed>(() => Object.fromEntries(seasonDetail.value.categories.map((category) => [category.id, `${category.groupName} · ${category.name}`])), @@ -307,7 +316,7 @@ export function useAdminCandidateManager() { adminMessage.value = '' adminError.value = '' editingId.value = 'new' - form.categoryId = categoryFilter.value ?? seasonDetail.value.categories[0]?.id ?? 0 + form.categoryId = preferredCreateCategoryId.value form.displayName = '' form.channelSlug = '' form.platform = 'Twitch' diff --git a/frontend/src/composables/useAdminApiDisconnectToast.ts b/frontend/src/composables/useAdminApiDisconnectToast.ts index a832557..19ae282 100644 --- a/frontend/src/composables/useAdminApiDisconnectToast.ts +++ b/frontend/src/composables/useAdminApiDisconnectToast.ts @@ -6,6 +6,7 @@ import { useAwardsStore } from '../stores/awards' const ADMIN_API_DISCONNECT_TOAST_KEY = 'admin-api-disconnected' const ADMIN_API_DISCONNECT_MESSAGE = 'Backend/API nicht erreichbar. Der Admin-Bereich bleibt eingeschränkt, bis die Verbindung wieder da ist.' +const ADMIN_API_DISCONNECT_TOAST_DELAY_MS = 1200 const RECONNECT_PROBE_INTERVAL_MS = 5000 export function useAdminApiDisconnectToast(isAdminRoute: ComputedRef) { @@ -13,6 +14,7 @@ export function useAdminApiDisconnectToast(isAdminRoute: ComputedRef) { const { dismissAdminToast, showAdminToast } = useAdminToast() let reconnectTimer: number | undefined + let disconnectToastTimer: number | undefined let reconnectProbeRunning = false function clearReconnectTimer() { @@ -21,6 +23,12 @@ export function useAdminApiDisconnectToast(isAdminRoute: ComputedRef) { reconnectTimer = undefined } + function clearDisconnectToastTimer() { + if (disconnectToastTimer === undefined || typeof window === 'undefined') return + window.clearTimeout(disconnectToastTimer) + disconnectToastTimer = undefined + } + function scheduleReconnectProbe() { if (reconnectTimer !== undefined || typeof window === 'undefined') return @@ -42,18 +50,30 @@ export function useAdminApiDisconnectToast(isAdminRoute: ComputedRef) { }, RECONNECT_PROBE_INTERVAL_MS) } + function scheduleDisconnectToast() { + if (disconnectToastTimer !== undefined || typeof window === 'undefined') return + + disconnectToastTimer = window.setTimeout(() => { + disconnectToastTimer = undefined + if (!isAdminRoute.value || awardsStore.apiMode !== 'fallback') return + + showAdminToast(ADMIN_API_DISCONNECT_MESSAGE, 'error', { + key: ADMIN_API_DISCONNECT_TOAST_KEY, + persistent: true, + }) + }, ADMIN_API_DISCONNECT_TOAST_DELAY_MS) + } + watch( () => isAdminRoute.value && awardsStore.apiMode === 'fallback', (shouldShowDisconnectToast) => { if (shouldShowDisconnectToast) { - showAdminToast(ADMIN_API_DISCONNECT_MESSAGE, 'error', { - key: ADMIN_API_DISCONNECT_TOAST_KEY, - persistent: true, - }) + scheduleDisconnectToast() scheduleReconnectProbe() return } + clearDisconnectToastTimer() clearReconnectTimer() dismissAdminToast(ADMIN_API_DISCONNECT_TOAST_KEY) }, @@ -61,6 +81,7 @@ export function useAdminApiDisconnectToast(isAdminRoute: ComputedRef) { ) onBeforeUnmount(() => { + clearDisconnectToastTimer() clearReconnectTimer() }) } diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue index 03817f2..500700d 100644 --- a/frontend/src/views/LoginView.vue +++ b/frontend/src/views/LoginView.vue @@ -28,6 +28,9 @@ const loginFormElement = ref(null) const passwordChangeFormElement = ref(null) const passwordChangeRequired = computed(() => Boolean(authStore.session?.mustChangePassword)) +const successMessageClass = computed(() => + passwordChangeRequired.value ? 'login-form__warning' : 'login-form__success', +) const redirectTarget = computed(() => { const redirect = Array.isArray(route.query.redirect) @@ -211,7 +214,7 @@ async function submitPasswordChange() { {{ errorMessage }}

-

{{ successMessage }}

@@ -231,7 +234,7 @@ async function submitPasswordChange() { {{ errorMessage }}

-

{{ successMessage }}

@@ -284,7 +287,7 @@ async function submitPasswordChange() { {{ errorMessage }}

-

{{ successMessage }}

@@ -535,6 +538,16 @@ async function submitPasswordChange() { font-weight: 800; } +.login-form__warning { + padding: 13px 15px; + border: 1px solid rgba(217, 119, 6, 0.24); + border-radius: 18px; + background: rgba(255, 251, 235, 0.96); + color: #b45309 !important; + font-size: 14px !important; + font-weight: 800; +} + .login-form__submit { min-height: 58px; border: 0; diff --git a/frontend/src/views/admin/AdminLayoutView.vue b/frontend/src/views/admin/AdminLayoutView.vue index a649b69..e4c9de6 100644 --- a/frontend/src/views/admin/AdminLayoutView.vue +++ b/frontend/src/views/admin/AdminLayoutView.vue @@ -260,13 +260,13 @@ watch(
-
-
+
+
-