Update release notes and deploy workspace
CI - Build & Verify / Build, Typecheck & Hygiene (push) Successful in 59s
CI - Build & Verify / Deploy to award.noveria.net (push) Failing after 53s

This commit is contained in:
AzuTear
2026-06-29 17:49:54 +02:00
parent c466348d18
commit 441ef2b850
196 changed files with 9279 additions and 39292 deletions
+185 -1
View File
@@ -208,6 +208,23 @@ public static partial class AdminSeasonManagementEndpoints
item.Candidate.Platform))
.ToArrayAsync();
var workflowRules = WorkflowRuleSettings.Read(season, settings);
var votingEntryRows = await db.VoteEntries
.AsNoTracking()
.Where(item => item.Category.SeasonId == seasonId)
.Select(item => new AdminVotingEntryRow(
item.BallotId,
item.CategoryId,
item.CandidateId))
.ToArrayAsync();
var votingWorkspace = BuildVotingWorkspace(
categories,
candidates,
pendingNominations,
resultItems,
votingEntryRows,
workflowRules);
var clipSubmissions = await db.ClipSubmissions
.AsNoTracking()
.Where(item => item.SeasonId == seasonId)
@@ -232,7 +249,7 @@ public static partial class AdminSeasonManagementEndpoints
season.Id,
season.Year,
season.Name,
NormalizeSeasonStreamUrl(season.ShowStreamUrl),
season.IsDemo,
season.CurrentPhase,
season.IsCurrent,
season.IsCommunityOnly,
@@ -244,6 +261,8 @@ public static partial class AdminSeasonManagementEndpoints
season.ReviewEndsAt,
season.ShowDate,
season.ShowStartsAt,
season.WinnersPublishedAt,
season.WinnersPublishedByTwitchId,
subcategoryTemplates,
categories,
candidates,
@@ -253,9 +272,15 @@ public static partial class AdminSeasonManagementEndpoints
settings?.TrackingReviewNotes ?? string.Empty,
trackingRules.Source.ShowManualReviewNotesInReview,
resultItems,
votingWorkspace,
clipSubmissions));
}
private sealed record AdminVotingEntryRow(
int BallotId,
int CategoryId,
int CandidateId);
private sealed record AdminNominationRow(
int Id,
int? CategoryId,
@@ -330,6 +355,165 @@ public static partial class AdminSeasonManagementEndpoints
item.ReviewedAt);
}
private static AdminVotingWorkspaceDto BuildVotingWorkspace(
AdminCategoryItemDto[] categories,
AdminCandidateItemDto[] candidates,
AdminNominationReviewItemDto[] pendingNominations,
AdminAwardResultItemDto[] results,
AdminVotingEntryRow[] voteEntries,
WorkflowRuleSetting[] workflowRules)
{
var resultMap = results.ToDictionary(item => item.CategoryId);
var totalBallots = voteEntries.Select(item => item.BallotId).Distinct().Count();
var voteCountByCategory = voteEntries
.GroupBy(item => item.CategoryId)
.ToDictionary(group => group.Key, group => group.Count());
var ballotCountByCategory = voteEntries
.GroupBy(item => item.CategoryId)
.ToDictionary(group => group.Key, group => group.Select(item => item.BallotId).Distinct().Count());
var voteCountByCandidate = voteEntries
.GroupBy(item => (item.CategoryId, item.CandidateId))
.ToDictionary(group => group.Key, group => group.Count());
var winnerPlacementsRule = WorkflowRuleSettings.Find(workflowRules, WorkflowRuleSettings.MaxWinnerPlacements);
var recommendedNominatorsRule = WorkflowRuleSettings.Find(workflowRules, WorkflowRuleSettings.RecommendedNominatorsPerSubcategory);
var winnerRequiresClipRule = WorkflowRuleSettings.Find(workflowRules, WorkflowRuleSettings.WinnerRequiresClip);
var workspaceItems = categories
.Select(category =>
{
var categoryCandidates = candidates
.Where(candidate => candidate.CategoryId == category.Id)
.ToArray();
var categoryVoteCount = voteCountByCategory.TryGetValue(category.Id, out var voteCount) ? voteCount : 0;
var categoryBallotCount = ballotCountByCategory.TryGetValue(category.Id, out var ballotCount) ? ballotCount : 0;
var nominationCount = categoryCandidates.Sum(candidate => Math.Max(candidate.NominationTally, 0));
var openReviewCount = pendingNominations.Count(item =>
item.CategoryId == category.Id
|| item.SuggestedCategoryId == category.Id);
var existingResult = resultMap.GetValueOrDefault(category.Id);
var maxVotes = categoryCandidates.Length == 0
? 0
: categoryCandidates.Max(candidate => voteCountByCandidate.GetValueOrDefault((category.Id, candidate.Id), 0));
var topVoteTieCount = maxVotes <= 0
? 0
: categoryCandidates.Count(candidate => voteCountByCandidate.GetValueOrDefault((category.Id, candidate.Id), 0) == maxVotes);
var leaderboard = categoryCandidates
.Select(candidate =>
{
var candidateVotes = voteCountByCandidate.GetValueOrDefault((category.Id, candidate.Id), 0);
var hasWinnerConflict = CandidateHasWinnerConflict(candidate, category.Id, results, winnerPlacementsRule);
return new AdminVotingCandidateRankDto(
candidate.Id,
candidate.DisplayName,
candidate.ChannelSlug,
candidate.Platform,
candidateVotes,
categoryVoteCount > 0 ? (int)Math.Round(candidateVotes * 100d / categoryVoteCount) : 0,
Math.Max(candidate.NominationTally, 0),
!string.IsNullOrWhiteSpace(candidate.ClipCompilationUrl),
string.IsNullOrWhiteSpace(candidate.ClipEmbedStatus) ? "unchecked" : candidate.ClipEmbedStatus,
hasWinnerConflict,
existingResult?.CandidateId == candidate.Id,
!string.Equals(candidate.AcceptanceStatus, "declined", StringComparison.OrdinalIgnoreCase),
maxVotes > 0 && candidateVotes == maxVotes && topVoteTieCount > 1);
})
.OrderByDescending(item => item.Votes)
.ThenByDescending(item => item.NominationTally)
.ThenBy(item => item.DisplayName, StringComparer.OrdinalIgnoreCase)
.ToArray();
var readyCandidates = leaderboard.Count(item => item.IsAccepted);
var leadingCandidate = leaderboard.FirstOrDefault();
var hasMissingClip = winnerRequiresClipRule.Enabled
&& leadingCandidate is not null
&& !leadingCandidate.HasClip;
var hasRuleConflict = leadingCandidate?.HasWinnerConflict ?? false;
var hasOpenReviews = openReviewCount > 0;
var hasSoftNominatorWarning = recommendedNominatorsRule.Enabled
&& nominationCount < Math.Max(1, recommendedNominatorsRule.Limit);
var winnerReady = existingResult is not null
|| leadingCandidate is not null
&& leadingCandidate.IsAccepted
&& !hasMissingClip
&& !hasRuleConflict
&& !hasOpenReviews;
return new AdminVotingCategoryWorkspaceItemDto(
category.Id,
category.GroupName,
category.Name,
category.SortOrder,
category.ViewerRangeMin,
category.ViewerRangeMax,
categoryVoteCount,
categoryBallotCount,
categoryCandidates.Length,
readyCandidates,
nominationCount,
openReviewCount,
existingResult is not null,
winnerReady,
topVoteTieCount > 1,
hasMissingClip,
hasRuleConflict,
hasOpenReviews,
hasSoftNominatorWarning,
leaderboard);
})
.OrderBy(item => item.SortOrder)
.ThenBy(item => item.GroupName, StringComparer.OrdinalIgnoreCase)
.ThenBy(item => item.CategoryName, StringComparer.OrdinalIgnoreCase)
.ToArray();
var summary = new AdminVotingWorkspaceSummaryDto(
voteEntries.Length,
totalBallots,
workspaceItems.Length,
workspaceItems.Count(item => item.VoteCount > 0),
workspaceItems.Count(item => item.WinnerReady),
workspaceItems.Count(item =>
item.HasMissingClip
|| item.HasRuleConflict
|| item.HasOpenReviews
|| item.HasTopVoteTie),
workspaceItems.Count(item => item.HasWinner));
return new AdminVotingWorkspaceDto(summary, workspaceItems);
}
private static bool CandidateHasWinnerConflict(
AdminCandidateItemDto candidate,
int categoryId,
AdminAwardResultItemDto[] results,
WorkflowRuleSetting winnerPlacementsRule)
{
if (!winnerPlacementsRule.Enabled)
{
return false;
}
var identityKey = candidate.StreamerIdentityId.HasValue
? $"identity:{candidate.StreamerIdentityId.Value}"
: WorkflowRuleSettings.CandidateIdentityKey(candidate.DisplayName, candidate.ChannelSlug);
var existingWinnerCount = results.Count(result =>
{
if (result.CategoryId == categoryId)
{
return false;
}
var resultIdentityKey = result.StreamerIdentityId.HasValue
? $"identity:{result.StreamerIdentityId.Value}"
: WorkflowRuleSettings.CandidateIdentityKey(result.CandidateDisplayName, result.CandidateChannelSlug);
return string.Equals(resultIdentityKey, identityKey, StringComparison.Ordinal);
});
return existingWinnerCount >= winnerPlacementsRule.Limit;
}
private static AdminNominationReviewGroupDto[] BuildNominationReviewGroups(
IEnumerable<AdminNominationRow> rows,
IEnumerable<dynamic> categoryRows,