- NCAAM R64/R32 winners no longer receive T5 floor points; only Sweet Sixteen winners entering the first scoring round get a floor - AFL T5-T6 and T7-T8 score as separate tiers (avg([5,6]) and avg([7,8])) instead of one shared avg([5-8]) pool - Eliminated teams now always show their correct final rank (e.g. T33 for NCAAM R64 losers) even during partial round scoring, by deriving the rank label from the total match count per round rather than the dynamic still-alive count - Discord notifications now fire when a drafted participant is eliminated with 0 points (e.g. R64 losers whose team score doesn't change) - Discord notifications no longer include all prior matches — scoped to the current batch via matchIds - Discord section headers have spacing; rank medals replaced with numbers - Rounds auto-complete when all matches are marked done; manual Complete Round dropdown removed - Fixed double Discord notification when the last match in a round triggers auto-complete: processPlayoffEvent now accepts skipRecalculate so the caller controls when the notification fires - Fixed falsy check on finalPosition=0 in calculateTeamScore - Undrafted 0-point participants filtered from Eliminated Teams display Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
103 lines
2.5 KiB
TypeScript
103 lines
2.5 KiB
TypeScript
interface DiscordEmbed {
|
|
title?: string;
|
|
description?: string;
|
|
color?: number;
|
|
footer?: { text: string };
|
|
}
|
|
|
|
interface DiscordWebhookPayload {
|
|
content?: string;
|
|
embeds?: DiscordEmbed[];
|
|
}
|
|
|
|
export async function sendDiscordWebhook(
|
|
webhookUrl: string,
|
|
payload: DiscordWebhookPayload
|
|
): Promise<void> {
|
|
const response = await fetch(webhookUrl, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const text = await response.text().catch(() => "");
|
|
throw new Error(`Discord webhook failed: ${response.status} ${text}`);
|
|
}
|
|
}
|
|
|
|
export interface StandingEntry {
|
|
teamId: string;
|
|
teamName: string;
|
|
totalPoints: number;
|
|
rank: number;
|
|
}
|
|
|
|
export interface ScoredMatch {
|
|
winnerName: string;
|
|
loserName: string;
|
|
}
|
|
|
|
export async function sendStandingsUpdateNotification({
|
|
webhookUrl,
|
|
seasonName,
|
|
standings,
|
|
previousStandings,
|
|
sportName,
|
|
eventName,
|
|
scoredMatches,
|
|
}: {
|
|
webhookUrl: string;
|
|
seasonName: string;
|
|
standings: StandingEntry[];
|
|
previousStandings: Map<string, number>;
|
|
sportName?: string;
|
|
eventName?: string;
|
|
scoredMatches?: ScoredMatch[];
|
|
}): Promise<void> {
|
|
const sections: string[] = [];
|
|
|
|
// Header: "Sport Name — Event Name"
|
|
if (sportName || eventName) {
|
|
const parts = [sportName, eventName].filter(Boolean);
|
|
sections.push(`**${parts.join(" — ")}**`);
|
|
}
|
|
|
|
// Scored matches section
|
|
if (scoredMatches && scoredMatches.length > 0) {
|
|
sections.push("\n**Scored Matches**");
|
|
for (const match of scoredMatches) {
|
|
sections.push(`• **${match.winnerName}** def. ${match.loserName}`);
|
|
}
|
|
}
|
|
|
|
// Standings section
|
|
sections.push("\n**Current Standings**");
|
|
for (const s of standings) {
|
|
const prev = previousStandings.get(s.teamId);
|
|
let delta = "";
|
|
if (prev !== undefined && prev !== s.totalPoints) {
|
|
const diff = Math.round(s.totalPoints - prev);
|
|
const sign = diff > 0 ? "+" : "";
|
|
delta = ` **(${sign}${diff} pts)**`;
|
|
}
|
|
sections.push(`${s.rank}. ${s.teamName} — ${Math.round(s.totalPoints)} pts${delta}`);
|
|
}
|
|
|
|
const MAX_DESCRIPTION = 4096;
|
|
let description = sections.join("\n");
|
|
if (description.length > MAX_DESCRIPTION) {
|
|
description = description.slice(0, MAX_DESCRIPTION - 3) + "...";
|
|
}
|
|
|
|
await sendDiscordWebhook(webhookUrl, {
|
|
embeds: [
|
|
{
|
|
title: `📊 Standings Update — ${seasonName}`,
|
|
description,
|
|
color: 0x5865f2, // Discord blurple
|
|
footer: { text: "brackt.com" },
|
|
},
|
|
],
|
|
});
|
|
}
|