Cap placements bracket progression placements at 100

This matters as we have arrays where the size is the amount of placements.
Previously some users put 100k + placements
This commit is contained in:
Kalle 2026-05-10 10:45:30 +03:00
parent 2d48d71ccc
commit f60966e11d
33 changed files with 121 additions and 1 deletions

View File

@ -636,7 +636,10 @@ function ErrorMessage({ error }: { error: Progression.ValidationError }) {
{bracketIdxsArr ? (
<> (Bracket {bracketIdxsArr.map((idx) => `#${idx + 1}`).join(", ")})</>
) : null}
: {t(`tournament:progression.error.${error.type}`)}
:{" "}
{t(`tournament:progression.error.${error.type}`, {
max: TOURNAMENT.PLACEMENT_MAX,
})}
</FormMessage>
);
}

View File

@ -442,6 +442,39 @@ describe("validatedSources - other rules", () => {
expect((error as any).bracketIdx).toEqual(1);
});
it("handles PLACEMENT_TOO_HIGH", () => {
const error = getValidatedBrackets([
{
settings: { teamsPerGroup: 200 },
type: "round_robin",
},
{
settings: {},
type: "single_elimination",
sources: [{ bracketId: "0", placements: "1-101" }],
},
]) as Progression.ValidationError;
expect(error.type).toBe("PLACEMENT_TOO_HIGH");
expect((error as any).bracketIdx).toEqual(1);
});
it("does not flag PLACEMENT_TOO_HIGH at the max boundary", () => {
const result = getValidatedBrackets([
{
settings: { teamsPerGroup: 200 },
type: "round_robin",
},
{
settings: {},
type: "single_elimination",
sources: [{ bracketId: "0", placements: "1-100" }],
},
]);
expect(Array.isArray(result)).toBe(true);
});
it("does not flag TOO_MANY_PLACEMENTS when larger round robin has valid high placements", () => {
const result = getValidatedBrackets([
{

View File

@ -68,6 +68,11 @@ export type ValidationError =
type: "TOO_MANY_PLACEMENTS";
bracketIdx: number;
}
// placements above the hard cap are nonsensical and bloat the settings JSON
| {
type: "PLACEMENT_TOO_HIGH";
bracketIdx: number;
}
// two brackets can not have the same name
| {
type: "DUPLICATE_BRACKET_NAME";
@ -254,6 +259,14 @@ export function bracketsToValidationError(
};
}
faultyBracketIdx = placementTooHigh(brackets);
if (typeof faultyBracketIdx === "number") {
return {
type: "PLACEMENT_TOO_HIGH",
bracketIdx: faultyBracketIdx,
};
}
faultyBracketIdx = nameMissing(brackets);
if (typeof faultyBracketIdx === "number") {
return {
@ -542,6 +555,22 @@ function tooManyPlacements(brackets: ParsedBracket[]) {
return null;
}
function placementTooHigh(brackets: ParsedBracket[]) {
for (const [bracketIdx, bracket] of brackets.entries()) {
for (const source of bracket.sources ?? []) {
if (
source.placements.some(
(placement) => placement > TOURNAMENT.PLACEMENT_MAX,
)
) {
return bracketIdx;
}
}
}
return null;
}
function nameMissing(brackets: ParsedBracket[]) {
for (const [bracketIdx, bracket] of brackets.entries()) {
if (!bracket.name) {

View File

@ -11,6 +11,7 @@ export const TOURNAMENT = {
MAX_GROUP_SIZE: 6,
MAX_BRACKETS_PER_TOURNAMENT: 10,
BRACKET_NAME_MAX_LENGTH: 32,
PLACEMENT_MAX: 100,
// just a fallback, normally this should be set by user explicitly
RR_DEFAULT_TEAM_COUNT_PER_GROUP: 4,
RR_TEAMS_PER_GROUP_OPTIONS: [3, 4, 5, 6],

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -170,6 +170,7 @@
"progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "",
"progression.error.GAP_IN_PLACEMENTS": "",
"progression.error.TOO_MANY_PLACEMENTS": "",
"progression.error.PLACEMENT_TOO_HIGH": "",
"progression.error.DUPLICATE_BRACKET_NAME": "",
"progression.error.NAME_MISSING": "",
"progression.error.NEGATIVE_PROGRESSION": "",

View File

@ -170,6 +170,7 @@
"progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "",
"progression.error.GAP_IN_PLACEMENTS": "",
"progression.error.TOO_MANY_PLACEMENTS": "",
"progression.error.PLACEMENT_TOO_HIGH": "",
"progression.error.DUPLICATE_BRACKET_NAME": "",
"progression.error.NAME_MISSING": "",
"progression.error.NEGATIVE_PROGRESSION": "",

View File

@ -170,6 +170,7 @@
"progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "Same placement leads to multiple brackets",
"progression.error.GAP_IN_PLACEMENTS": "Gap in placements that advance",
"progression.error.TOO_MANY_PLACEMENTS": "Too many placements (more than teams in groups)",
"progression.error.PLACEMENT_TOO_HIGH": "Placement is too high (max {{max}})",
"progression.error.DUPLICATE_BRACKET_NAME": "Duplicate bracket name",
"progression.error.NAME_MISSING": "Bracket name missing",
"progression.error.NEGATIVE_PROGRESSION": "Negative progression only possible for double elimination",

View File

@ -172,6 +172,7 @@
"progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "La misma clasificación lleva a varios cuadros",
"progression.error.GAP_IN_PLACEMENTS": "Hay un hueco en las clasificaciones que avanzan",
"progression.error.TOO_MANY_PLACEMENTS": "Demasiadas clasificaciones (más que equipos en grupos)",
"progression.error.PLACEMENT_TOO_HIGH": "",
"progression.error.DUPLICATE_BRACKET_NAME": "Nombre de cuadro duplicado",
"progression.error.NAME_MISSING": "Falta el nombre del cuadro",
"progression.error.NEGATIVE_PROGRESSION": "La progresión negativa solo es posible en eliminación doble",

View File

@ -172,6 +172,7 @@
"progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "",
"progression.error.GAP_IN_PLACEMENTS": "",
"progression.error.TOO_MANY_PLACEMENTS": "",
"progression.error.PLACEMENT_TOO_HIGH": "",
"progression.error.DUPLICATE_BRACKET_NAME": "",
"progression.error.NAME_MISSING": "",
"progression.error.NEGATIVE_PROGRESSION": "",

View File

@ -172,6 +172,7 @@
"progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "",
"progression.error.GAP_IN_PLACEMENTS": "",
"progression.error.TOO_MANY_PLACEMENTS": "",
"progression.error.PLACEMENT_TOO_HIGH": "",
"progression.error.DUPLICATE_BRACKET_NAME": "",
"progression.error.NAME_MISSING": "",
"progression.error.NEGATIVE_PROGRESSION": "",

View File

@ -172,6 +172,7 @@
"progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "Same placement leads to multiple brackets",
"progression.error.GAP_IN_PLACEMENTS": "Gap in placements that advance",
"progression.error.TOO_MANY_PLACEMENTS": "Too many placements (more than teams in groups)",
"progression.error.PLACEMENT_TOO_HIGH": "",
"progression.error.DUPLICATE_BRACKET_NAME": "Duplicate bracket name",
"progression.error.NAME_MISSING": "Bracket name missing",
"progression.error.NEGATIVE_PROGRESSION": "Negative progression only possible for double elimination",

View File

@ -172,6 +172,7 @@
"progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "",
"progression.error.GAP_IN_PLACEMENTS": "",
"progression.error.TOO_MANY_PLACEMENTS": "",
"progression.error.PLACEMENT_TOO_HIGH": "",
"progression.error.DUPLICATE_BRACKET_NAME": "",
"progression.error.NAME_MISSING": "",
"progression.error.NEGATIVE_PROGRESSION": "",

View File

@ -172,6 +172,7 @@
"progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "La stessa posizione porta a più bracket",
"progression.error.GAP_IN_PLACEMENTS": "Vuoto tra le posizioni che avanzano",
"progression.error.TOO_MANY_PLACEMENTS": "Troppe posizioni (più dei team nella fase a gironi)",
"progression.error.PLACEMENT_TOO_HIGH": "",
"progression.error.DUPLICATE_BRACKET_NAME": "Nome bracket duplicato",
"progression.error.NAME_MISSING": "Nome bracket mancante",
"progression.error.NEGATIVE_PROGRESSION": "La progressione negativa è disponibile solo in doppia eliminazione",

View File

@ -166,6 +166,7 @@
"progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "同じ順位はブラケットを多数作ります",
"progression.error.GAP_IN_PLACEMENTS": "前に進む順位にギャップがあります",
"progression.error.TOO_MANY_PLACEMENTS": "順位がありすぎます。(グループに入っているチームより多いです)",
"progression.error.PLACEMENT_TOO_HIGH": "",
"progression.error.DUPLICATE_BRACKET_NAME": "ブラケットの名前が重複しています",
"progression.error.NAME_MISSING": "ブラケットの名前がありません",
"progression.error.NEGATIVE_PROGRESSION": "逆の進行はダブルエリ三ネーションの時のみ可能です",

View File

@ -166,6 +166,7 @@
"progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "",
"progression.error.GAP_IN_PLACEMENTS": "",
"progression.error.TOO_MANY_PLACEMENTS": "",
"progression.error.PLACEMENT_TOO_HIGH": "",
"progression.error.DUPLICATE_BRACKET_NAME": "",
"progression.error.NAME_MISSING": "",
"progression.error.NEGATIVE_PROGRESSION": "",

View File

@ -170,6 +170,7 @@
"progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "",
"progression.error.GAP_IN_PLACEMENTS": "",
"progression.error.TOO_MANY_PLACEMENTS": "",
"progression.error.PLACEMENT_TOO_HIGH": "",
"progression.error.DUPLICATE_BRACKET_NAME": "",
"progression.error.NAME_MISSING": "",
"progression.error.NEGATIVE_PROGRESSION": "",

View File

@ -174,6 +174,7 @@
"progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "",
"progression.error.GAP_IN_PLACEMENTS": "",
"progression.error.TOO_MANY_PLACEMENTS": "",
"progression.error.PLACEMENT_TOO_HIGH": "",
"progression.error.DUPLICATE_BRACKET_NAME": "",
"progression.error.NAME_MISSING": "",
"progression.error.NEGATIVE_PROGRESSION": "",

View File

@ -172,6 +172,7 @@
"progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "",
"progression.error.GAP_IN_PLACEMENTS": "",
"progression.error.TOO_MANY_PLACEMENTS": "",
"progression.error.PLACEMENT_TOO_HIGH": "",
"progression.error.DUPLICATE_BRACKET_NAME": "",
"progression.error.NAME_MISSING": "",
"progression.error.NEGATIVE_PROGRESSION": "",

View File

@ -174,6 +174,7 @@
"progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "Одинаковые места приводят к нескольким сеткам",
"progression.error.GAP_IN_PLACEMENTS": "Разрыв между местами, которые проходят на следующий этап",
"progression.error.TOO_MANY_PLACEMENTS": "Слишком много мест (больше чем команд в группах)",
"progression.error.PLACEMENT_TOO_HIGH": "",
"progression.error.DUPLICATE_BRACKET_NAME": "Дубликат имени сетки",
"progression.error.NAME_MISSING": "Имя сетки отсутствует",
"progression.error.NEGATIVE_PROGRESSION": "Отрицательная прогрессия возможна только в Double Elimination турнирах",

View File

@ -166,6 +166,7 @@
"progression.error.SAME_PLACEMENT_TO_MULTIPLE_BRACKETS": "",
"progression.error.GAP_IN_PLACEMENTS": "",
"progression.error.TOO_MANY_PLACEMENTS": "",
"progression.error.PLACEMENT_TOO_HIGH": "",
"progression.error.DUPLICATE_BRACKET_NAME": "",
"progression.error.NAME_MISSING": "",
"progression.error.NEGATIVE_PROGRESSION": "",

View File

@ -0,0 +1,38 @@
const PLACEMENT_MAX = 100;
export function up(db) {
const rows = db
.prepare(/* sql */ `select "id", "settings" from "Tournament"`)
.all();
const updates = [];
for (const row of rows) {
const settings = JSON.parse(row.settings);
const progression = settings.bracketProgression;
if (!Array.isArray(progression)) continue;
let changed = false;
for (const bracket of progression) {
for (const source of bracket.sources ?? []) {
if (!Array.isArray(source.placements)) continue;
const filtered = source.placements.filter((p) => p <= PLACEMENT_MAX);
if (filtered.length !== source.placements.length) {
source.placements = filtered;
changed = true;
}
}
}
if (changed) {
updates.push({ id: row.id, settings: JSON.stringify(settings) });
}
}
const update = db.prepare(
/* sql */ `update "Tournament" set "settings" = ? where "id" = ?`,
);
for (const u of updates) {
update.run(u.settings, u.id);
}
}