Skip to content

Feature/tournament component - knockout stage#919

Open
SzBeni2003 wants to merge 106 commits into
stagingfrom
feature/tournament-component
Open

Feature/tournament component - knockout stage#919
SzBeni2003 wants to merge 106 commits into
stagingfrom
feature/tournament-component

Conversation

@SzBeni2003

@SzBeni2003 SzBeni2003 commented Jul 26, 2025

Copy link
Copy Markdown
Contributor

Sportversenyek vezetésére szolgáló komponens.

  • A csapatvezetők tudnak a létrehozott versenyre jelentkezni a verseny oldalán található jelentkezés gombbal.
  • Az egyenes kieséses szakasz létrehozásánál a tournamentId megjegyzését elkerülendő, egy "create/{tournamentId} oldalon hozható az létre, ahonnan a submit a referer linkből állapítja meg, melyik versenyhez készült az új szakasz. (Van erre jobb megoldás új thymeleaf template létrehozása nélkül?)
  • Az admin oldalon meg lehet adni az ágrajzon a csapatok elhelyezkedését (azaz, hogy kinek milyen más csapatokkal kell megküzdenie a győzelemért) - ezt egy új thymeleaf template segítségével valósítottam meg.
  • A meccsek eredményeinek bevitelére is új thymeleaf oldal készült

Summary by CodeRabbit

  • New Features
    • Added a full tournament area with list and detail pages, including knockout brackets and (placeholder) group-stage views.
    • Implemented join/unregister actions from the UI, with automatic refresh and user feedback.
    • Added tournament administration for managing stages, setting seeds (with ordering), and entering match results (including forfeit options).
  • Bug Fixes
    • Improved permission- and visibility-based access handling, returning safe fallbacks when tournaments or actions are restricted or unavailable.

NOT TESTED!
NOT TESTED!
…to feature/tournament-component

# Conflicts:
#	backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt
#	backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentDetailedView.kt
#	backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt
#	frontend/src/pages/tournament/components/Tournament.tsx
#	frontend/src/util/paths.ts
#	frontend/src/util/views/tournament.view.ts
# Conflicts:
#	backend/src/main/kotlin/hu/bme/sch/cmsch/config/ComponentLoadConfig.kt
#	backend/src/main/kotlin/hu/bme/sch/cmsch/controller/admin/OneDeepEntityPage.kt
#	frontend/src/App.tsx
#	frontend/src/api/contexts/config/types.ts
#	frontend/src/api/hooks/queryKeys.ts
#	frontend/src/util/paths.ts
#	helm/cmsch/templates/cmsch-config.yml
#	helm/cmsch/values.yaml
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 931c9470-47ab-411d-a498-096abbf8e7e9

📥 Commits

Reviewing files that changed from the base of the PR and between 44f367d and 8aa0532.

📒 Files selected for processing (7)
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentDetailedView.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageController.kt
  • frontend/src/components/ui/tabs.tsx
  • frontend/src/pages/tournament/components/Tournament.tsx
  • frontend/src/util/views/tournament.view.ts
💤 Files with no reviewable changes (1)
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageController.kt
🚧 Files skipped from review as they are similar to previous changes (5)
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentDetailedView.kt
  • frontend/src/pages/tournament/components/Tournament.tsx
  • frontend/src/util/views/tournament.view.ts
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt

📝 Walkthrough

Walkthrough

This PR adds an end-to-end tournament feature with backend domain models, persistence, services, API/admin workflows, permissions, configuration, templates, seeded data, and a frontend tournament listing, detail view, registration flow, and bracket rendering.

Changes

Tournament Feature

Layer / File(s) Summary
Domain models and persistence
backend/.../tournament/*Dto.kt, *Entity.kt, *Repository.kt, TournamentDetailedView.kt
Adds tournament, stage, match, participant, status, view, and repository projection models with persistence and comparison logic.
Tournament services
TournamentComponent.kt, TournamentService.kt, TournamentStageService.kt
Implements visibility, participant registration, stage seeding, bracket generation, match progression, and aggregation.
API and admin workflows
TournamentApiController.kt, TournamentController.kt, TournamentStageController.kt, TournamentMatchController.kt, TournamentComponentController.kt
Adds public tournament endpoints and administrative tournament, stage, seed, match, and score management flows.
Permissions, configuration, fixtures, and templates
PermissionsService.kt, config/*, helm/*, templates/*, style4.css, tournament-features.md
Adds permission validators, feature flags, seeded tournament data, administrative templates, styling, and lifecycle documentation.
Frontend tournament experience
frontend/src/pages/tournament/*, frontend/src/api/hooks/tournament/*, frontend/src/util/views/tournament.view.ts, App.tsx, paths.ts
Adds typed API hooks, routes, list/detail pages, join/cancel actions, knockout bracket rendering, group-stage scaffolding, and match cards.

Estimated code review effort: 4 (Complex) | ~75 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately reflects a real part of the change: adding tournament-component knockout-stage support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/tournament-component
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feature/tournament-component

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (13)
backend/src/main/resources/templates/matchAdmin.html-63-63 (1)

63-63: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Missing </html> closing tag.

The file ends with </body> but never closes the <html> element opened on line 2.

🔧 Proposed fix
 </body>
+</html>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/main/resources/templates/matchAdmin.html` at line 63, The
template ends after closing the body but never closes the root html element.
Update matchAdmin.html so the markup opened in the main template is properly
terminated with a closing html tag after the existing body close, ensuring the
document structure is complete.

Source: Linters/SAST tools

backend/src/main/resources/templates/matchScore.html-33-33 (1)

33-33: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Invalid CSS box-shadow value — missing px unit.

box-shadow: 5 4px 16px rgba(0,0,0,0.3) — the 5 lacks a length unit, making the entire declaration invalid. The popup renders without a shadow.

🎨 Proposed fix
-        box-shadow: 5 4px 16px rgba(0,0,0,0.3);
+        box-shadow: 5px 4px 16px rgba(0,0,0,0.3);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/main/resources/templates/matchScore.html` at line 33, The
`box-shadow` declaration in the `matchScore.html` template is invalid because
the first offset value is missing a length unit. Update the CSS in the popup
styling so the `box-shadow` values are all valid lengths, using the existing
shadow declaration near the popup styles in `matchScore.html`.
backend/src/main/resources/templates/matchScore.html-116-136 (1)

116-136: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard the action buttons by match status TournamentMatchController.scoreMatchPage sets readOnly = false, so a finished match still renders these buttons even though the popup is omitted; the onclick handlers then hit missing elements. Apply the same NOT_STARTED/IN_PROGRESS condition to the three <object> blocks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/main/resources/templates/matchScore.html` around lines 116 - 136,
The action buttons in matchScore.html are only hidden by readOnly, so finished
matches still render the forfeit/save controls even though the score popup
inputs are missing. Update the three <object> blocks around the away-forfeit,
home-forfeit, and save-button elements to use the same match-status guard as the
popup rendering, keyed off the scoreMatchPage flow and the
NOT_STARTED/IN_PROGRESS states, so these buttons only appear when the score
inputs exist.
frontend/src/pages/tournament/components/Match.tsx-10-10 (1)

10-10: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Tie scores render red for both sides.

When score1 === score2, score1 > score2 is false, so both participants get the losing color. A draw should use a neutral color.

🐛 Proposed fix
-  return score1 > score2 ? 'green.600' : 'red.600'
+  return score1 > score2 ? 'green.600' : score1 === score2 ? 'gray.600' : 'red.600'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/tournament/components/Match.tsx` at line 10, The score
color logic in the Match component treats ties as losses because the current
comparison only checks whether one score is greater than the other. Update the
helper or inline ternary in Match.tsx so equal scores map to a neutral color
instead of the losing color, while preserving the existing winning and losing
colors for non-tie cases.
frontend/src/pages/tournament/components/Tournament.tsx-112-112 (1)

112-112: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add key prop to CustomTabButton in the stages map.

Rendering CustomTabButton inside .map() without a key prop triggers a React warning and can cause incorrect reconciliation if the stages list reorders.

🐛 Proposed fix — add key prop
             {tournament.stages.map((stage, index) => (
-              <CustomTabButton value={`stage-${index}`}>{stage.name}</CustomTabButton>
+              <CustomTabButton key={stage.id} value={`stage-${index}`}>{stage.name}</CustomTabButton>
             ))}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/tournament/components/Tournament.tsx` at line 112, The
stage tabs rendered in the stages .map() are missing a React key, which can
cause reconciliation issues. Update the CustomTabButton usage in Tournament to
pass a stable key prop alongside value, using the stage item’s unique identifier
if available or the index only as a fallback. Keep the change localized to the
mapped CustomTabButton element so React can track each stage tab correctly.
backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageEntity.kt-117-124 (1)

117-124: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

equals() treats two unsaved entities (id=0) as equal.

Unlike TournamentEntity.equals() which guards with id != 0 && id == other.id, this implementation only checks id != other.id. Two distinct unsaved TournamentStageEntity instances (both with id = 0) would be considered equal, which can cause subtle bugs in Set/Map operations before persistence.

🐛 Proposed fix — add id != 0 guard consistent with TournamentEntity
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (other !is TournamentStageEntity) return false

-        if (id != other.id) return false
-
-        return true
+        return id != 0 && id == other.id
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageEntity.kt`
around lines 117 - 124, `TournamentStageEntity.equals()` currently treats two
new entities with `id = 0` as equal; update the equality check to match the
guard used in `TournamentEntity.equals()` so only persisted entities can compare
equal by id. In the `TournamentStageEntity` class, modify the `equals`
implementation to require a non-zero id before comparing ids, keeping the
existing `this === other` and type checks intact.
backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt-82-87 (1)

82-87: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

kickoffTime type contradicts @Column(nullable = true).

The field is Long (non-nullable) but the column is declared nullable = true. If the database ever stores NULL, Hibernate will throw a NullPointerException when loading the entity. Either make the field Long? or change the column to nullable = false.

🔧 Proposed fix — make the field nullable to match the column
-    var kickoffTime: Long = 0,
+    var kickoffTime: Long? = null,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt`
around lines 82 - 87, The kickoffTime mapping in TournamentMatchEntity is
inconsistent because the JPA column allows NULL while the Kotlin property is
non-nullable. Update the TournamentMatchEntity.kickoffTime field to match the
database nullability by making it nullable, or alternatively make the column
non-nullable if NULL should never be stored; keep the annotations and property
type aligned so Hibernate can load the entity safely.
backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageEntity.kt-109-109 (1)

109-109: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard rounds() against non-positive participantCount.

log2(0.0) returns -Infinity and log2 of a negative number returns NaN; ceil of either yields NaN/-Infinity, and .toInt() coerces to 0. While the UI enforces min = 1, there is no runtime or DB constraint preventing participantCount = 0, which would silently produce 0 rounds instead of an error.

🛡️ Proposed fix — add a guard clause
-    fun rounds() = ceil(log2(participantCount.toDouble())).toInt()
+    fun rounds(): Int {
+        require(participantCount > 0) { "participantCount must be positive, was $participantCount" }
+        return ceil(log2(participantCount.toDouble())).toInt()
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageEntity.kt`
at line 109, Guard TournamentStageEntity.rounds() against non-positive
participantCount by adding an explicit check before calling log2/ceil. If
participantCount is 0 or negative, fail fast with a clear error or otherwise
handle it intentionally instead of letting the calculation silently return 0.
Keep the fix localized to rounds() and preserve the existing behavior for valid
positive counts.
backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageController.kt-298-300 (1)

298-300: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Error message is lost on redirect.

model.addAttribute("error", ...) followed by a redirect: discards the attribute (model does not survive a redirect). The seed page will render without the error. Use RedirectAttributes.addFlashAttribute instead. The same pattern recurs at Lines 326-327 and 342-345.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageController.kt`
around lines 298 - 300, The error message is being added to the regular model
before a redirect in TournamentStageController, so it is lost after navigation.
Update the affected redirect paths in the seed-related methods to use
RedirectAttributes.addFlashAttribute instead of model.addAttribute for the
"error" message, including the repeated cases in the same controller. Make sure
the methods handling the seed edit/save redirects keep the flash attribute so
the error is available on the redirected page.
backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageController.kt-318-322 (1)

318-322: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

stageStatus param can NPE on malformed requests.

allRequestParams["stageStatus"]!! throws a NullPointerException (500) if the parameter is absent. Handle the missing case gracefully like the other validated inputs.

🛡️ Proposed fix
-        val stageStatus: StageStatus = allRequestParams["stageStatus"]!!.let { when (it) {
-            "CREATED" -> StageStatus.CREATED
-            "SET" -> StageStatus.SET
-            else -> return "error"
-        }}
+        val stageStatus: StageStatus = when (allRequestParams["stageStatus"]) {
+            "CREATED" -> StageStatus.CREATED
+            "SET" -> StageStatus.SET
+            else -> return "error"
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageController.kt`
around lines 318 - 322, The stageStatus parsing in TournamentStageController can
throw a NullPointerException because allRequestParams["stageStatus"]!! assumes
the parameter is always present. Update the validation flow around stageStatus
to handle a missing value gracefully, similar to the other checked request
params, and keep the existing mapping to StageStatus.CREATED and StageStatus.SET
while returning the error path for absent or invalid input.
backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageService.kt-244-248 (1)

244-248: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Filtered result is discarded; the time-window/status filter is a no-op.

matches.filter { ... }.filter { ... } returns a new list that is never assigned, so Line 248 returns the full unfiltered matches. The status and closeMatchesTimeWindow filtering has no effect. (Function is currently unused per the comment, but the logic is still broken.)

🐛 Proposed fix
-        matches.filter { it.status in listOf(MatchStatus.IN_PROGRESS, MatchStatus.NOT_STARTED) }
-            .filter { (it.kickoffTime - clock.getTime()) in -timeFrame..timeFrame }
-
-        return matches.sortedBy { it.kickoffTime }
+        return matches
+            .filter { it.status in listOf(MatchStatus.IN_PROGRESS, MatchStatus.NOT_STARTED) }
+            .filter { (it.kickoffTime - clock.getTime()) in -timeFrame..timeFrame }
+            .sortedBy { it.kickoffTime }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageService.kt`
around lines 244 - 248, The time-window and status filtering in
TournamentStageService’s match selection is currently a no-op because the
chained filter calls discard their result before the return. Update the logic in
the match-sorting/filtering method so the filtered list is actually used,
applying both the MatchStatus and closeMatchesTimeWindow conditions before
sorting and returning. Keep the existing TournamentStageService method
structure, but ensure the final returned collection reflects the filtered
matches rather than the original matches list.
backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt-81-86 (1)

81-86: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Align group joinability with teamRegister Privileged users still satisfy joinChangable for OwnershipType.GROUP, but teamRegister returns INSUFFICIENT_PERMISSIONS, so the detail view can expose a join action that always fails.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt`
around lines 81 - 86, Align the joinability check in TournamentService’s
joinable logic with the teamRegister permission rules so the detail view never
offers a group join action that will fail. Update the `joinChangable` condition
to match `teamRegister` for `OwnershipType.GROUP`, ensuring only users who can
actually register a group are counted as joinable; keep the existing participant
limit, deadline, and joinable checks intact.
backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt-46-64 (1)

46-64: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Add the tournament access guard to registration endpoints. register/unregister only require an authenticated user; they bypass the minRole/showTournamentsAtAll checks used by the read endpoints, so restricted or hidden tournaments can still be joined directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt`
around lines 46 - 64, The registerTeam and unregisterTeam endpoints in
TournamentApiController bypass the same tournament access checks used by the
read endpoints, so restricted or hidden tournaments can still be joined. Update
these endpoints to validate the tournament with the existing access guard logic
(the minRole/showTournamentsAtAll checks used for tournament reads) before
calling tournamentService.register or tournamentService.unregister, and return
an appropriate forbidden/insufficient-permissions status when access is denied.
🧹 Nitpick comments (9)
backend/src/main/resources/templates/matchScore.html (1)

67-70: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove commented-out code.

The commented-out <object> block for tournament title display should be removed or uncommented if needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/main/resources/templates/matchScore.html` around lines 67 - 70,
The commented-out tournament title block in the matchScore template should be
removed unless it is meant to be used. Clean up the template by deleting the
unused <object> section around the tournament title display, or uncommenting it
if the UI still needs it; use the surrounding label and th:text binding to
locate the block.
frontend/src/util/paths.ts (1)

29-29: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Naming inconsistency: Paths.TOURNAMENT (singular) vs AbsolutePaths.TOURNAMENTS/ApiPaths.TOURNAMENTS (plural).

Other entries in this file maintain consistent plurality between Paths and AbsolutePaths (e.g., TEAMS/TEAMS, TOKEN/TOKEN). The singular Paths.TOURNAMENT paired with plural AbsolutePaths.TOURNAMENTS and ApiPaths.TOURNAMENTS breaks this convention and could cause confusion.

Also applies to: 63-63, 112-112

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/util/paths.ts` at line 29, `Paths.TOURNAMENT` is inconsistent
with the plural naming used in `AbsolutePaths.TOURNAMENTS` and
`ApiPaths.TOURNAMENTS`; align the `Paths` enum/member name with the other route
constants. Update the related route symbols in `paths.ts` so the same plurality
is used across `Paths`, `AbsolutePaths`, and `ApiPaths`, and make sure any
references to the tournament path constant are renamed consistently.
frontend/src/pages/tournament/tournamentList.page.tsx (1)

22-32: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Use <Link> for tournament items
frontend/src/pages/tournament/tournamentList.page.tsx: replace the raw <a href> with Link so tournament navigation stays in-app and avoids a full reload. This page is inconsistent with the other list pages.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/tournament/tournamentList.page.tsx` around lines 22 - 32,
The tournament list item navigation is using a raw anchor tag, which causes a
full page reload instead of in-app routing. Update the tournament item link in
tournamentList.page.tsx to use the existing Link component used elsewhere in the
app, keeping the same destination built from AbsolutePaths.TOURNAMENTS and
tournament.id. Make sure the tournament title still renders as the clickable
label and that the surrounding list/map structure remains unchanged.
frontend/src/pages/tournament/components/KnockoutStage.tsx (1)

7-8: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer standard import type over import() type expressions.

Using import('...').Type is valid but non-idiomatic and harder to read. Standard import type is clearer and consistent with the rest of the codebase.

♻️ Proposed refactor
-type TournamentStageView = import('../../../util/views/tournament.view').TournamentStageView
-type MatchView = import('../../../util/views/tournament.view').MatchView
+import type { TournamentStageView, MatchView } from '../../../util/views/tournament.view'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/tournament/components/KnockoutStage.tsx` around lines 7 -
8, Replace the `import('...').Type` aliases in `KnockoutStage` with standard
`import type` imports. Update the `TournamentStageView` and `MatchView`
references to use a proper type-only import from `tournament.view` so the
declarations remain clear, consistent, and idiomatic with the rest of the
codebase.
frontend/src/pages/tournament/components/Tournament.tsx (2)

50-79: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove redundant double refetch() calls.

Both joinActionResponseCallback (line 36) and cancelActionResponseCallback (line 44) already call refetch() when the response is OK. The onSuccess callbacks in joinTournament (lines 55-57) and cancelJoinTournament (lines 70-72) call refetch() again for the same condition, resulting in duplicate invocations.

♻️ Proposed fix — remove redundant refetch from onSuccess callbacks
   const joinTournament = () => {
     if (tournament.tournament.joinEnabled) {
       joinMutation.mutate(tournament.tournament.id, {
         onSuccess: (response: TournamentJoinResponses) => {
           joinActionResponseCallback(response)
-          if (response === TournamentJoinResponses.OK) {
-            refetch()
-          }
         },
         onError: () => {
           toast({ variant: 'destructive', title: 'Hiba történt a versenyre való jelentkezés során.' })
         }
       })
     }
   }
   const cancelJoinTournament = () => {
     if (tournament.tournament.joinCancellable) {
       cancelMutation.mutate(tournament.tournament.id, {
         onSuccess: (response: TournamentCancelResponses) => {
           cancelActionResponseCallback(response)
-          if (response === TournamentCancelResponses.OK) {
-            refetch()
-          }
         },
         onError: () => {
           toast({ variant: 'destructive', title: 'Hiba történt a versenyre való jelentkezés visszavonása során.' })
         }
       })
     }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/tournament/components/Tournament.tsx` around lines 50 -
79, The `joinTournament` and `cancelJoinTournament` handlers in `Tournament.tsx`
are triggering duplicate refetches because `joinActionResponseCallback` and
`cancelActionResponseCallback` already refetch on `TournamentJoinResponses.OK`
and `TournamentCancelResponses.OK`. Remove the extra `refetch()` calls from the
`onSuccess` branches inside `joinMutation.mutate` and `cancelMutation.mutate`,
and let the existing callback functions own the refresh behavior.

128-129: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use StageType constants instead of string literals.

The StageType enum is already imported-adjacent in tournament.view.ts but not used here. Comparing against raw strings 'KNOCKOUT' and 'STAGE' bypasses TypeScript's type safety — if the enum values ever change, these comparisons silently break.

♻️ Proposed fix — use StageType constants
+import {
+  StageType,
+  TournamentCancelResponseMessages,
+  TournamentCancelResponses,
+  TournamentJoinResponseMessages,
+  TournamentJoinResponses
+} from '`@/util/views/tournament.view.ts`'
-              {stage.type === 'KNOCKOUT' && <KnockoutStage key={stage.id} stage={stage} />}
-              {stage.type === 'STAGE' && <GroupStage key={stage.id} stage={stage} />}
+              {stage.type === StageType.KNOCKOUT && <KnockoutStage key={stage.id} stage={stage} />}
+              {stage.type === StageType.STAGE && <GroupStage key={stage.id} stage={stage} />}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/tournament/components/Tournament.tsx` around lines 128 -
129, The stage rendering in Tournament.tsx uses raw string comparisons for
stage.type, which should be replaced with StageType enum constants for type
safety. Update the conditional checks in the Tournament component to compare
against the corresponding StageType members instead of 'KNOCKOUT' and 'STAGE',
using the existing StageType import so the logic stays aligned with the enum
definitions.
backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentController.kt (1)

19-23: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the redundant MenuService injection.

MenuService is injected twice: menuService (used in the super call) and service (Line 23), which is unused. Drop the service parameter.

♻️ Proposed fix
     private val tournamentService: TournamentService,
     private val auditLogService: AuditLogService,
-    private val storageService: StorageService,
-    service: MenuService
+    private val storageService: StorageService
 ) : ComponentApiBase(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentController.kt`
around lines 19 - 23, The constructor in TournamentComponentController injects
MenuService twice, but only the menuService parameter is used in the super call.
Remove the redundant unused service: MenuService parameter from the class
constructor and keep the existing menuService injection that is already passed
to the parent, ensuring the remaining constructor parameters still match the
controller’s dependencies.
backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageService.kt (1)

262-274: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Collapse the duplicated BYE branches.

Both the home-winner and away-winner branches do exactly the same thing (set MatchStatus.BYE and save); only the else needs to remain distinct.

♻️ Proposed refactor
-                if (winner.teamId == match.homeTeamId) {
-                    match.status = MatchStatus.BYE
-                    matchRepository.save(match)
-                } else if (winner.teamId == match.awayTeamId) {
-                    match.status = MatchStatus.BYE
-                    matchRepository.save(match)
-                } else {
-                    // Handle case where winner is not one of the teams in the match
-                    throw IllegalStateException("Winner team ID ${winner.teamId} does not match any team in match ${match.id}.")
-                }
+                if (winner.teamId == match.homeTeamId || winner.teamId == match.awayTeamId) {
+                    match.status = MatchStatus.BYE
+                    matchRepository.save(match)
+                } else {
+                    throw IllegalStateException("Winner team ID ${winner.teamId} does not match any team in match ${match.id}.")
+                }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageService.kt`
around lines 262 - 274, The winner-handling logic in TournamentStageService is
duplicating the same BYE update for both home and away winners. Refactor the
conditional inside the matches loop so that the `winner()` result only checks
whether `winner.teamId` matches either `match.homeTeamId` or `match.awayTeamId`,
then performs the shared `match.status = MatchStatus.BYE` and
`matchRepository.save(match)` once; keep the `IllegalStateException` branch only
for the invalid-team case.
backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageController.kt (1)

211-219: 🩺 Stability & Availability | 🔵 Trivial | 🏗️ Heavy lift

Deriving stage type and tournament id from the Referer header is fragile.

Both stageType and tournamentId are parsed out of the Referer, and the mapping requires headers = ["Referer"]. Referrer can be stripped by browser Referrer-Policy, proxies, or privacy settings, in which case stage creation silently 404s or redirects. Prefer passing tournamentId/type as an explicit hidden form field or path variable so creation does not depend on the referer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageController.kt`
around lines 211 - 219, The stage creation flow in TournamentStageController
currently depends on parsing stageType and tournamentId from the Referer header,
which is fragile and can fail when the header is missing or stripped. Update the
create handler to accept these values explicitly instead of deriving them from
Referer, ideally by using hidden form fields or path/query parameters in the
stage creation form and controller method. Keep the StageType mapping logic in
TournamentStageController, but read the incoming type and tournament id from the
explicit request data so creation no longer depends on header presence.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/StageResultDto.kt`:
- Around line 15-27: StageResultDto.compareTo is comparing different fields for
the null-detailedStats case, which breaks Comparable consistency. Update the
comparison in compareTo so it uses the same property on both sides of the
comparison, likely comparing this.highestSeed with other.highestSeed if that is
the intended ordering. Keep the rest of the ordering logic in
StageResultDto.compareTo and compareValuesBy unchanged, but ensure all branches
compare matching fields to preserve transitivity.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt`:
- Around line 140-152: `TournamentMatchEntity.winner()` contains dead null
checks on `homeSeed` and `awaySeed` even though those fields are non-nullable,
so update the guard to match the real “unset” sentinel used by this model (for
example, check for 0 instead of null) or change the seed properties to nullable
if null is intended. Keep the rest of the winner selection logic unchanged, and
ensure the early-return validation in `winner()` consistently reflects how seeds
and team assignment are represented in `TournamentMatchEntity`.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt`:
- Around line 152-158: The getResultsInStage method in TournamentService
currently parses every split line without guarding against an empty participants
field, so an empty string can still reach objectMapper.readValue and throw.
Update getResultsInStage to mirror the empty-input handling used by
getParticipants: check stage.get().participants for blank/empty content before
splitting, and filter out empty lines from the split result before mapping to
StageResultDto so only real entries are parsed.
- Around line 285-288: Delete tournament stage matches before removing the
stages, because deleteAllByTournamentId in
TournamentService.deleteStagesForTournament bypasses
TournamentStageController.onEntityDeleted and leaves orphaned match stageId
references. Update deleteStagesForTournament to either reuse the existing stage
deletion path that already clears matches or explicitly remove the associated
matches first, so getAggregatedMatchesByTournamentId no longer encounters
missing stage entries.
- Around line 160-176: The tournament aggregation in
getAggregatedMatchesByTournamentId assumes every aggregated stage still exists,
but deleted stages can leave stale stageId values and cause a null crash. Update
the loop over matchRepository.findAllAggregated() to safely handle missing
entries from stages[aggregatedStage.stageId] instead of forcing a non-null
access, and either skip those aggregates or otherwise account for them before
building the MatchGroupDto list.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageService.kt`:
- Around line 59-76: `transferTeamsForStage` can throw
`IndexOutOfBoundsException` when `getResultsForStage(stage)` returns fewer
entries than `stage.participantCount`, because the `participants[i]` loop always
iterates up to `participantCount`. Fix this by ensuring the list is padded or
bounded before assigning `initialSeed` in
`TournamentStageService.transferTeamsForStage`, so every slot up to
`stage.participantCount` has a `StageResultDto` (including bye placeholders)
before the seeding loop runs.
- Around line 133-149: The bounds check in createRoundMatchesForGroupStageGroup
is inverted, so valid group numbers are rejected while out-of-range values may
pass. Update the require condition to fail only when the requested group exceeds
the stage’s groupCount, and keep the existing error message consistent with that
validation. Use the createRoundMatchesForGroupStageGroup function and the
stage.groupCount/group parameters to locate and correct the check.

In `@backend/src/main/resources/templates/matchScore.html`:
- Line 100: The score fields in matchScore.html are using an unsupported
Thymeleaf attribute, so the values are not rendered. Update the home and away
score elements in the matchScore template to use th:text instead of th:number,
keeping the existing match.homeTeamScore and match.awayTeamScore bindings so the
actual numbers display correctly.

In `@backend/src/main/resources/templates/seedSettings.html`:
- Line 80: The seed settings template currently renders a user-visible
placeholder in the stageLevel>1 branch of the table cell, so update the
th:if/th:text logic in seedSettings.html to remove the literal placeholder from
the “Előző eredmény” column. Either wire this cell to the real previous-result
value in the relevant template binding or hide the column/cell until the feature
is implemented, using the existing stageLevel condition as the locator.

In `@frontend/src/pages/tournament/components/Match.tsx`:
- Around line 8-11: getScoreColor in Match.tsx is returning Chakra semantic
tokens that are then passed into inline style color values, so the browser never
resolves them. Update the score rendering in Match to use Chakra’s color prop
(or another Chakra-aware styling path) instead of style={{ color: ... }}, or
change getScoreColor to return real CSS color values so the completed score text
is actually colored.

---

Minor comments:
In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt`:
- Around line 46-64: The registerTeam and unregisterTeam endpoints in
TournamentApiController bypass the same tournament access checks used by the
read endpoints, so restricted or hidden tournaments can still be joined. Update
these endpoints to validate the tournament with the existing access guard logic
(the minRole/showTournamentsAtAll checks used for tournament reads) before
calling tournamentService.register or tournamentService.unregister, and return
an appropriate forbidden/insufficient-permissions status when access is denied.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt`:
- Around line 82-87: The kickoffTime mapping in TournamentMatchEntity is
inconsistent because the JPA column allows NULL while the Kotlin property is
non-nullable. Update the TournamentMatchEntity.kickoffTime field to match the
database nullability by making it nullable, or alternatively make the column
non-nullable if NULL should never be stored; keep the annotations and property
type aligned so Hibernate can load the entity safely.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt`:
- Around line 81-86: Align the joinability check in TournamentService’s joinable
logic with the teamRegister permission rules so the detail view never offers a
group join action that will fail. Update the `joinChangable` condition to match
`teamRegister` for `OwnershipType.GROUP`, ensuring only users who can actually
register a group are counted as joinable; keep the existing participant limit,
deadline, and joinable checks intact.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageController.kt`:
- Around line 298-300: The error message is being added to the regular model
before a redirect in TournamentStageController, so it is lost after navigation.
Update the affected redirect paths in the seed-related methods to use
RedirectAttributes.addFlashAttribute instead of model.addAttribute for the
"error" message, including the repeated cases in the same controller. Make sure
the methods handling the seed edit/save redirects keep the flash attribute so
the error is available on the redirected page.
- Around line 318-322: The stageStatus parsing in TournamentStageController can
throw a NullPointerException because allRequestParams["stageStatus"]!! assumes
the parameter is always present. Update the validation flow around stageStatus
to handle a missing value gracefully, similar to the other checked request
params, and keep the existing mapping to StageStatus.CREATED and StageStatus.SET
while returning the error path for absent or invalid input.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageEntity.kt`:
- Around line 117-124: `TournamentStageEntity.equals()` currently treats two new
entities with `id = 0` as equal; update the equality check to match the guard
used in `TournamentEntity.equals()` so only persisted entities can compare equal
by id. In the `TournamentStageEntity` class, modify the `equals` implementation
to require a non-zero id before comparing ids, keeping the existing `this ===
other` and type checks intact.
- Line 109: Guard TournamentStageEntity.rounds() against non-positive
participantCount by adding an explicit check before calling log2/ceil. If
participantCount is 0 or negative, fail fast with a clear error or otherwise
handle it intentionally instead of letting the calculation silently return 0.
Keep the fix localized to rounds() and preserve the existing behavior for valid
positive counts.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageService.kt`:
- Around line 244-248: The time-window and status filtering in
TournamentStageService’s match selection is currently a no-op because the
chained filter calls discard their result before the return. Update the logic in
the match-sorting/filtering method so the filtered list is actually used,
applying both the MatchStatus and closeMatchesTimeWindow conditions before
sorting and returning. Keep the existing TournamentStageService method
structure, but ensure the final returned collection reflects the filtered
matches rather than the original matches list.

In `@backend/src/main/resources/templates/matchAdmin.html`:
- Line 63: The template ends after closing the body but never closes the root
html element. Update matchAdmin.html so the markup opened in the main template
is properly terminated with a closing html tag after the existing body close,
ensuring the document structure is complete.

In `@backend/src/main/resources/templates/matchScore.html`:
- Line 33: The `box-shadow` declaration in the `matchScore.html` template is
invalid because the first offset value is missing a length unit. Update the CSS
in the popup styling so the `box-shadow` values are all valid lengths, using the
existing shadow declaration near the popup styles in `matchScore.html`.
- Around line 116-136: The action buttons in matchScore.html are only hidden by
readOnly, so finished matches still render the forfeit/save controls even though
the score popup inputs are missing. Update the three <object> blocks around the
away-forfeit, home-forfeit, and save-button elements to use the same
match-status guard as the popup rendering, keyed off the scoreMatchPage flow and
the NOT_STARTED/IN_PROGRESS states, so these buttons only appear when the score
inputs exist.

In `@frontend/src/pages/tournament/components/Match.tsx`:
- Line 10: The score color logic in the Match component treats ties as losses
because the current comparison only checks whether one score is greater than the
other. Update the helper or inline ternary in Match.tsx so equal scores map to a
neutral color instead of the losing color, while preserving the existing winning
and losing colors for non-tie cases.

In `@frontend/src/pages/tournament/components/Tournament.tsx`:
- Line 112: The stage tabs rendered in the stages .map() are missing a React
key, which can cause reconciliation issues. Update the CustomTabButton usage in
Tournament to pass a stable key prop alongside value, using the stage item’s
unique identifier if available or the index only as a fallback. Keep the change
localized to the mapped CustomTabButton element so React can track each stage
tab correctly.

---

Nitpick comments:
In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentController.kt`:
- Around line 19-23: The constructor in TournamentComponentController injects
MenuService twice, but only the menuService parameter is used in the super call.
Remove the redundant unused service: MenuService parameter from the class
constructor and keep the existing menuService injection that is already passed
to the parent, ensuring the remaining constructor parameters still match the
controller’s dependencies.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageController.kt`:
- Around line 211-219: The stage creation flow in TournamentStageController
currently depends on parsing stageType and tournamentId from the Referer header,
which is fragile and can fail when the header is missing or stripped. Update the
create handler to accept these values explicitly instead of deriving them from
Referer, ideally by using hidden form fields or path/query parameters in the
stage creation form and controller method. Keep the StageType mapping logic in
TournamentStageController, but read the incoming type and tournament id from the
explicit request data so creation no longer depends on header presence.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageService.kt`:
- Around line 262-274: The winner-handling logic in TournamentStageService is
duplicating the same BYE update for both home and away winners. Refactor the
conditional inside the matches loop so that the `winner()` result only checks
whether `winner.teamId` matches either `match.homeTeamId` or `match.awayTeamId`,
then performs the shared `match.status = MatchStatus.BYE` and
`matchRepository.save(match)` once; keep the `IllegalStateException` branch only
for the invalid-team case.

In `@backend/src/main/resources/templates/matchScore.html`:
- Around line 67-70: The commented-out tournament title block in the matchScore
template should be removed unless it is meant to be used. Clean up the template
by deleting the unused <object> section around the tournament title display, or
uncommenting it if the UI still needs it; use the surrounding label and th:text
binding to locate the block.

In `@frontend/src/pages/tournament/components/KnockoutStage.tsx`:
- Around line 7-8: Replace the `import('...').Type` aliases in `KnockoutStage`
with standard `import type` imports. Update the `TournamentStageView` and
`MatchView` references to use a proper type-only import from `tournament.view`
so the declarations remain clear, consistent, and idiomatic with the rest of the
codebase.

In `@frontend/src/pages/tournament/components/Tournament.tsx`:
- Around line 50-79: The `joinTournament` and `cancelJoinTournament` handlers in
`Tournament.tsx` are triggering duplicate refetches because
`joinActionResponseCallback` and `cancelActionResponseCallback` already refetch
on `TournamentJoinResponses.OK` and `TournamentCancelResponses.OK`. Remove the
extra `refetch()` calls from the `onSuccess` branches inside
`joinMutation.mutate` and `cancelMutation.mutate`, and let the existing callback
functions own the refresh behavior.
- Around line 128-129: The stage rendering in Tournament.tsx uses raw string
comparisons for stage.type, which should be replaced with StageType enum
constants for type safety. Update the conditional checks in the Tournament
component to compare against the corresponding StageType members instead of
'KNOCKOUT' and 'STAGE', using the existing StageType import so the logic stays
aligned with the enum definitions.

In `@frontend/src/pages/tournament/tournamentList.page.tsx`:
- Around line 22-32: The tournament list item navigation is using a raw anchor
tag, which causes a full page reload instead of in-app routing. Update the
tournament item link in tournamentList.page.tsx to use the existing Link
component used elsewhere in the app, keeping the same destination built from
AbsolutePaths.TOURNAMENTS and tournament.id. Make sure the tournament title
still renders as the clickable label and that the surrounding list/map structure
remains unchanged.

In `@frontend/src/util/paths.ts`:
- Line 29: `Paths.TOURNAMENT` is inconsistent with the plural naming used in
`AbsolutePaths.TOURNAMENTS` and `ApiPaths.TOURNAMENTS`; align the `Paths`
enum/member name with the other route constants. Update the related route
symbols in `paths.ts` so the same plurality is used across `Paths`,
`AbsolutePaths`, and `ApiPaths`, and make sure any references to the tournament
path constant are renamed consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cf5e269b-5b85-4625-90dd-fb98d6a63d3d

📥 Commits

Reviewing files that changed from the base of the PR and between 10e47d0 and 44f367d.

📒 Files selected for processing (53)
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/MatchGroupDto.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/ParticipantDto.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/StageGroupDto.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/StageResultDto.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentCancelStatus.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentController.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentEntityConfiguration.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentController.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentDetailedView.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentJoinDto.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentJoinStatus.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchController.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentPreviewView.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentRepository.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageController.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageEntity.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageRepository.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageService.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/tournament-features.md
  • backend/src/main/kotlin/hu/bme/sch/cmsch/config/ComponentLoadConfig.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/config/TestConfig.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt
  • backend/src/main/resources/config/application-env.properties
  • backend/src/main/resources/config/application.properties
  • backend/src/main/resources/static/style4.css
  • backend/src/main/resources/templates/matchAdmin.html
  • backend/src/main/resources/templates/matchScore.html
  • backend/src/main/resources/templates/seedSettings.html
  • frontend/src/App.tsx
  • frontend/src/api/contexts/config/types.ts
  • frontend/src/api/hooks/queryKeys.ts
  • frontend/src/api/hooks/tournament/actions/useTournamentCancelMutation.ts
  • frontend/src/api/hooks/tournament/actions/useTournamentJoinMutation.ts
  • frontend/src/api/hooks/tournament/queries/useTournamentListQuery.ts
  • frontend/src/api/hooks/tournament/queries/useTournamentQuery.ts
  • frontend/src/pages/tournament/components/GroupStage.tsx
  • frontend/src/pages/tournament/components/KnockoutBracket.tsx
  • frontend/src/pages/tournament/components/KnockoutStage.tsx
  • frontend/src/pages/tournament/components/Match.tsx
  • frontend/src/pages/tournament/components/Tournament.tsx
  • frontend/src/pages/tournament/tournament.page.tsx
  • frontend/src/pages/tournament/tournamentList.page.tsx
  • frontend/src/pages/tournament/util/matchTree.ts
  • frontend/src/util/paths.ts
  • frontend/src/util/views/tournament.view.ts
  • helm/cmsch/templates/cmsch-config.yml
  • helm/cmsch/values.yaml

Comment on lines +15 to +27
override fun compareTo(other: StageResultDto): Int {
if (this.stageId != other.stageId) {
return this.stageId.compareTo(other.stageId)
}
if (this.detailedStats == null && other.detailedStats == null) {
return -this.initialSeed.compareTo(other.highestSeed)
}
if (this.detailedStats == null) {
return -1 // null detailed stats are considered better (knockout stages are later)
}
return compareValuesBy(this, other,
{ it.detailedStats?:GroupStageResults()}, {it.initialSeed}, {it.highlighted})
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

compareTo compares mismatched fields, violating the Comparable contract.

Line 20 compares this.initialSeed against other.highestSeed — two different fields. This breaks transitivity. For example, with three teams all having detailedStats == null:

  • A (initialSeed=1, highestSeed=10), B (initialSeed=5, highestSeed=5), C (initialSeed=10, highestSeed=1)
  • A vs B → -1.compareTo(5) → negative → A < B
  • B vs C → -5.compareTo(1) → negative → B < C
  • A vs C → -1.compareTo(1) → zero → A == C

A < B and B < C but A == C violates the transitivity requirement of Comparable.

🐛 Proposed fix — compare the same field on both sides
     if (this.detailedStats == null && other.detailedStats == null) {
-        return -this.initialSeed.compareTo(other.highestSeed)
+        return -this.initialSeed.compareTo(other.initialSeed)
     }

If the business intent is actually to compare highestSeed, use this.highestSeed.compareTo(other.highestSeed) instead. The key requirement is comparing the same field on both sides.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
override fun compareTo(other: StageResultDto): Int {
if (this.stageId != other.stageId) {
return this.stageId.compareTo(other.stageId)
}
if (this.detailedStats == null && other.detailedStats == null) {
return -this.initialSeed.compareTo(other.highestSeed)
}
if (this.detailedStats == null) {
return -1 // null detailed stats are considered better (knockout stages are later)
}
return compareValuesBy(this, other,
{ it.detailedStats?:GroupStageResults()}, {it.initialSeed}, {it.highlighted})
}
override fun compareTo(other: StageResultDto): Int {
if (this.stageId != other.stageId) {
return this.stageId.compareTo(other.stageId)
}
if (this.detailedStats == null && other.detailedStats == null) {
return -this.initialSeed.compareTo(other.initialSeed)
}
if (this.detailedStats == null) {
return -1 // null detailed stats are considered better (knockout stages are later)
}
return compareValuesBy(this, other,
{ it.detailedStats?:GroupStageResults()}, {it.initialSeed}, {it.highlighted})
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/StageResultDto.kt`
around lines 15 - 27, StageResultDto.compareTo is comparing different fields for
the null-detailedStats case, which breaks Comparable consistency. Update the
comparison in compareTo so it uses the same property on both sides of the
comparison, likely comparing this.highestSeed with other.highestSeed if that is
the intended ordering. Keep the rest of the ordering logic in
StageResultDto.compareTo and compareValuesBy unchanged, but ensure all branches
compare matching fields to preserve transitivity.

Comment on lines +140 to +152
fun winner(): ParticipantDto? {
return when {
homeSeed == null || awaySeed == null -> null // No teams set
homeTeamId == null || awayTeamId == null -> null // No teams set
homeTeamId == 0 -> ParticipantDto(awayTeamId!!, awayTeamName)
awayTeamId == 0 -> ParticipantDto(homeTeamId!!, homeTeamName)
status != MatchStatus.FINISHED -> null
homeTeamScore == null || awayTeamScore == null -> null
homeTeamScore!! > awayTeamScore!! -> ParticipantDto(homeTeamId ?: 0, homeTeamName)
awayTeamScore!! > homeTeamScore!! -> ParticipantDto(awayTeamId ?: 0, awayTeamName)
else -> null // Draw or no winner
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fix dead null checks on non-nullable seed fields in winner().

homeSeed and awaySeed are declared as non-nullable Int (lines 55–56, 69–70), so the checks homeSeed == null || awaySeed == null on line 142 are always false — this is dead code that gives a false sense of validation. The intent was likely to check whether seeds are unset (i.e., 0), or the fields should be Int? if null is a valid "no team assigned" sentinel.

🐛 Proposed fix — check for unset seeds (value 0) instead of null
     return when {
-        homeSeed == null || awaySeed == null -> null // No teams set
+        homeSeed == 0 || awaySeed == 0 -> null // No seeds set
         homeTeamId == null || awayTeamId == null -> null // No teams set
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun winner(): ParticipantDto? {
return when {
homeSeed == null || awaySeed == null -> null // No teams set
homeTeamId == null || awayTeamId == null -> null // No teams set
homeTeamId == 0 -> ParticipantDto(awayTeamId!!, awayTeamName)
awayTeamId == 0 -> ParticipantDto(homeTeamId!!, homeTeamName)
status != MatchStatus.FINISHED -> null
homeTeamScore == null || awayTeamScore == null -> null
homeTeamScore!! > awayTeamScore!! -> ParticipantDto(homeTeamId ?: 0, homeTeamName)
awayTeamScore!! > homeTeamScore!! -> ParticipantDto(awayTeamId ?: 0, awayTeamName)
else -> null // Draw or no winner
}
}
fun winner(): ParticipantDto? {
return when {
homeSeed == 0 || awaySeed == 0 -> null // No seeds set
homeTeamId == null || awayTeamId == null -> null // No teams set
homeTeamId == 0 -> ParticipantDto(awayTeamId!!, awayTeamName)
awayTeamId == 0 -> ParticipantDto(homeTeamId!!, homeTeamName)
status != MatchStatus.FINISHED -> null
homeTeamScore == null || awayTeamScore == null -> null
homeTeamScore!! > awayTeamScore!! -> ParticipantDto(homeTeamId ?: 0, homeTeamName)
awayTeamScore!! > homeTeamScore!! -> ParticipantDto(awayTeamId ?: 0, awayTeamName)
else -> null // Draw or no winner
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt`
around lines 140 - 152, `TournamentMatchEntity.winner()` contains dead null
checks on `homeSeed` and `awaySeed` even though those fields are non-nullable,
so update the guard to match the real “unset” sentinel used by this model (for
example, check for 0 instead of null) or change the seed properties to nullable
if null is intended. Keep the rest of the winner selection logic unchanged, and
ensure the early-return validation in `winner()` consistently reflects how seeds
and team assignment are represented in `TournamentMatchEntity`.

Comment on lines +152 to +158
fun getResultsInStage(tournamentId: Int, stageId: Int): List<StageResultDto> {
val stage = stageRepository.findById(stageId)
if (stage.isEmpty || stage.get().tournamentId != tournamentId) {
return emptyList()
}
return stage.get().participants.split("\n").map { objectMapper.readValue(it, StageResultDto::class.java) }
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard against empty stage participants before parsing.

Unlike getParticipants (Lines 134, 143), this path never checks for an empty participants string nor filters empty lines. When participants == "", split("\n") yields [""] and objectMapper.readValue("", ...) throws, failing the request.

🛡️ Proposed fix
-        val stage = stageRepository.findById(stageId)
-        if (stage.isEmpty || stage.get().tournamentId != tournamentId) {
-            return emptyList()
-        }
-        return stage.get().participants.split("\n").map { objectMapper.readValue(it, StageResultDto::class.java) }
+        val stage = stageRepository.findById(stageId)
+        if (stage.isEmpty || stage.get().tournamentId != tournamentId || stage.get().participants.isEmpty()) {
+            return emptyList()
+        }
+        return stage.get().participants.split("\n")
+            .filter { it.isNotEmpty() }
+            .map { objectMapper.readValue(it, StageResultDto::class.java) }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun getResultsInStage(tournamentId: Int, stageId: Int): List<StageResultDto> {
val stage = stageRepository.findById(stageId)
if (stage.isEmpty || stage.get().tournamentId != tournamentId) {
return emptyList()
}
return stage.get().participants.split("\n").map { objectMapper.readValue(it, StageResultDto::class.java) }
}
fun getResultsInStage(tournamentId: Int, stageId: Int): List<StageResultDto> {
val stage = stageRepository.findById(stageId)
if (stage.isEmpty || stage.get().tournamentId != tournamentId || stage.get().participants.isEmpty()) {
return emptyList()
}
return stage.get().participants.split("\n")
.filter { it.isNotEmpty() }
.map { objectMapper.readValue(it, StageResultDto::class.java) }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt`
around lines 152 - 158, The getResultsInStage method in TournamentService
currently parses every split line without guarding against an empty participants
field, so an empty string can still reach objectMapper.readValue and throw.
Update getResultsInStage to mirror the empty-input handling used by
getParticipants: check stage.get().participants for blank/empty content before
splitting, and filter out empty lines from the split result before mapping to
StageResultDto so only real entries are parsed.

Comment on lines +160 to +176
fun getAggregatedMatchesByTournamentId(): List<MatchGroupDto> {
val tournaments = findAll().associateBy { it.id }
val stages = stageRepository.findAll().associateBy { it.id }
val aggregatedByStageId = matchRepository.findAllAggregated()
val aggregated = mutableMapOf<Int, Long>()
for(aggregatedStage in aggregatedByStageId) {
aggregated[stages[aggregatedStage.stageId]!!.tournamentId] = aggregated.getOrDefault(stages[aggregatedStage.stageId]!!.tournamentId, 0) + aggregatedStage.matchCount
}
return aggregated.map {
MatchGroupDto(
it.key,
tournaments[it.key]?.title ?:"",
tournaments[it.key]?.location ?:"",
it.value.toInt()
)
}.sortedByDescending { it.matchCount }
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the target service and the referenced delete path
git ls-files | rg 'TournamentService\.kt$|StageRepository|MatchRepository|MatchGroupDto'

# Show the relevant file structure and line ranges around the cited methods
ast-grep outline backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt --view expanded

# Read the relevant section around getAggregatedMatchesByTournamentId
sed -n '140,210p' backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt

# Read the deleteStagesForTournament section
sed -n '260,320p' backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt

Repository: kir-dev/cmsch

Length of output: 5798


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect repository methods involved in the aggregation and deletion paths
ast-grep outline backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageRepository.kt --view expanded
ast-grep outline backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt --view expanded

sed -n '1,220p' backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageRepository.kt
printf '\n---\n'
sed -n '1,240p' backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt

Repository: kir-dev/cmsch

Length of output: 3551


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect stage/match entity mappings and any cascading or foreign-key behavior
ast-grep outline backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageEntity.kt --view expanded
ast-grep outline backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt --view expanded

sed -n '1,260p' backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageEntity.kt
printf '\n---\n'
sed -n '1,260p' backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt

Repository: kir-dev/cmsch

Length of output: 12420


Guard the stage lookup in tournament aggregation.
deleteStagesForTournament() removes stage rows without touching matches, so matchRepository.findAllAggregated() can still return stageIds that no longer exist. stages[aggregatedStage.stageId]!! then NPEs and breaks the whole admin aggregation; skip missing stages or clean up the related matches first.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt`
around lines 160 - 176, The tournament aggregation in
getAggregatedMatchesByTournamentId assumes every aggregated stage still exists,
but deleted stages can leave stale stageId values and cause a null crash. Update
the loop over matchRepository.findAllAggregated() to safely handle missing
entries from stages[aggregatedStage.stageId] instead of forcing a non-null
access, and either skip those aggregates or otherwise account for them before
building the MatchGroupDto list.

Comment on lines +285 to +288
@Transactional
fun deleteStagesForTournament(tournament: TournamentEntity) {
stageRepository.deleteAllByTournamentId(tournament.id)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant files first
git ls-files | rg 'backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/(TournamentService.kt|TournamentStageController.kt|.*Repository.kt|.*Entity.kt|.*Match.*\.kt)$|backend/src/main/kotlin/hu/bme/sch/cmsch/component/.*Match.*\.kt$'

printf '\n--- TournamentService outline ---\n'
ast-grep outline backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt --view expanded

printf '\n--- TournamentStageController outline ---\n'
ast-grep outline backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageController.kt --view expanded

printf '\n--- Search for deleteAllByTournamentId and onEntityDeleted ---\n'
rg -n "deleteAllByTournamentId|onEntityDeleted|getAggregatedMatchesByTournamentId|stageId|matchRepository|stageRepository" backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament backend/src/main/kotlin/hu/bme/sch/cmsch/component -S

Repository: kir-dev/cmsch

Length of output: 22991


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the relevant slices once the files are located
sed -n '1,260p' backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt
printf '\n--- controller ---\n'
sed -n '1,260p' backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageController.kt
printf '\n--- repositories / entities / match aggregation ---\n'
rg -n "interface .*Repository|class .*Entity|data class .*Entity|getAggregatedMatchesByTournamentId|!!" backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament backend/src/main/kotlin/hu/bme/sch/cmsch/component -S

Repository: kir-dev/cmsch

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- TournamentService around aggregation and deleteStagesForTournament ---'
sed -n '155,295p' backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt

printf '\n%s\n' '--- TournamentStageController onEntityDeleted ---'
sed -n '345,370p' backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageController.kt

printf '\n%s\n' '--- TournamentController onEntityDeleted ---'
sed -n '50,75p' backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentController.kt

printf '\n%s\n' '--- TournamentStageEntity ---'
sed -n '1,220p' backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageEntity.kt

printf '\n%s\n' '--- TournamentMatchEntity ---'
sed -n '1,240p' backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt

printf '\n%s\n' '--- TournamentStageRepository and TournamentMatchRepository ---'
sed -n '1,120p' backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageRepository.kt
printf '\n---\n'
sed -n '1,120p' backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt

Repository: kir-dev/cmsch

Length of output: 21410


Delete stage matches before removing tournament stages. deleteAllByTournamentId bypasses TournamentStageController.onEntityDeleted, so matches keep dangling stageIds. Those orphaned rows make getAggregatedMatchesByTournamentId() hit stages[aggregatedStage.stageId]!!. Delete the matches first, or reuse the stage deletion path that already clears them.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt`
around lines 285 - 288, Delete tournament stage matches before removing the
stages, because deleteAllByTournamentId in
TournamentService.deleteStagesForTournament bypasses
TournamentStageController.onEntityDeleted and leaves orphaned match stageId
references. Update deleteStagesForTournament to either reuse the existing stage
deletion path that already clears matches or explicitly remove the associated
matches first, so getAggregatedMatchesByTournamentId no longer encounters
missing stage entries.

Comment on lines +59 to +76
fun transferTeamsForStage(stage: TournamentStageEntity): String {
val teamSeeds = (1..stage.participantCount).asIterable().toList()
var participants = getResultsForStage(stage)
if (participants.size >= stage.participantCount) {
participants =
participants.subList(0, stage.participantCount).map { StageResultDto(it.teamId, it.teamName) }
}
for (i in 0 until stage.participantCount) {
participants[i].initialSeed = teamSeeds[i]
}
val parts = mutableListOf<StageResultDto>()
parts.addAll(participants)
for (i in stage.participantCount + 1 until 2.0.pow(stage.rounds()).toInt() + 1) {
parts.add(StageResultDto(teamId = 0, teamName = "ByeGame", initialSeed = i))
}
stage.participants = parts.joinToString("\n") { objectMapper.writeValueAsString(it) }
return stage.participants
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

IndexOutOfBoundsException when fewer teams than participantCount.

The truncation only runs when participants.size >= stage.participantCount. If fewer teams advanced/joined, participants keeps its smaller size, yet the loop iterates 0 until stage.participantCount and reads participants[i], throwing out of bounds during stage creation (onEntityChanged).

🐛 Proposed fix
-        for (i in 0 until stage.participantCount) {
+        for (i in participants.indices) {
             participants[i].initialSeed = teamSeeds[i]
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun transferTeamsForStage(stage: TournamentStageEntity): String {
val teamSeeds = (1..stage.participantCount).asIterable().toList()
var participants = getResultsForStage(stage)
if (participants.size >= stage.participantCount) {
participants =
participants.subList(0, stage.participantCount).map { StageResultDto(it.teamId, it.teamName) }
}
for (i in 0 until stage.participantCount) {
participants[i].initialSeed = teamSeeds[i]
}
val parts = mutableListOf<StageResultDto>()
parts.addAll(participants)
for (i in stage.participantCount + 1 until 2.0.pow(stage.rounds()).toInt() + 1) {
parts.add(StageResultDto(teamId = 0, teamName = "ByeGame", initialSeed = i))
}
stage.participants = parts.joinToString("\n") { objectMapper.writeValueAsString(it) }
return stage.participants
}
fun transferTeamsForStage(stage: TournamentStageEntity): String {
val teamSeeds = (1..stage.participantCount).asIterable().toList()
var participants = getResultsForStage(stage)
if (participants.size >= stage.participantCount) {
participants =
participants.subList(0, stage.participantCount).map { StageResultDto(it.teamId, it.teamName) }
}
for (i in participants.indices) {
participants[i].initialSeed = teamSeeds[i]
}
val parts = mutableListOf<StageResultDto>()
parts.addAll(participants)
for (i in stage.participantCount + 1 until 2.0.pow(stage.rounds()).toInt() + 1) {
parts.add(StageResultDto(teamId = 0, teamName = "ByeGame", initialSeed = i))
}
stage.participants = parts.joinToString("\n") { objectMapper.writeValueAsString(it) }
return stage.participants
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageService.kt`
around lines 59 - 76, `transferTeamsForStage` can throw
`IndexOutOfBoundsException` when `getResultsForStage(stage)` returns fewer
entries than `stage.participantCount`, because the `participants[i]` loop always
iterates up to `participantCount`. Fix this by ensuring the list is padded or
bounded before assigning `initialSeed` in
`TournamentStageService.transferTeamsForStage`, so every slot up to
`stage.participantCount` has a `StageResultDto` (including bye placeholders)
before the seeding loop runs.

Comment on lines +133 to +149
@Transactional
fun createRoundMatchesForGroupStageGroup(stage: TournamentStageEntity, group: Int) {
require(stage.type == StageType.GROUP) {
"Stage type must be GROUP to create round matches."
}
require(stage.groupCount < group) {
"Group number $group is out of bounds for stage with ${stage.groupCount} groups."
}
val matches = mutableListOf<TournamentMatchEntity>()
val groupSize = let {
val min = floor(stage.participantCount.toDouble() / stage.groupCount).toInt()
if (stage.participantCount % stage.groupCount < group) {
min + 1
} else {
min
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Inverted bounds check rejects valid group numbers.

require(stage.groupCount < group) throws whenever group <= groupCount — i.e. exactly for the valid group numbers — which contradicts the "out of bounds" message. The intent is to fail when group exceeds the group count.

🐛 Proposed fix
-        require(stage.groupCount < group) {
+        require(group in 1..stage.groupCount) {
             "Group number $group is out of bounds for stage with ${stage.groupCount} groups."
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Transactional
fun createRoundMatchesForGroupStageGroup(stage: TournamentStageEntity, group: Int) {
require(stage.type == StageType.GROUP) {
"Stage type must be GROUP to create round matches."
}
require(stage.groupCount < group) {
"Group number $group is out of bounds for stage with ${stage.groupCount} groups."
}
val matches = mutableListOf<TournamentMatchEntity>()
val groupSize = let {
val min = floor(stage.participantCount.toDouble() / stage.groupCount).toInt()
if (stage.participantCount % stage.groupCount < group) {
min + 1
} else {
min
}
}
`@Transactional`
fun createRoundMatchesForGroupStageGroup(stage: TournamentStageEntity, group: Int) {
require(stage.type == StageType.GROUP) {
"Stage type must be GROUP to create round matches."
}
require(group in 1..stage.groupCount) {
"Group number $group is out of bounds for stage with ${stage.groupCount} groups."
}
val matches = mutableListOf<TournamentMatchEntity>()
val groupSize = let {
val min = floor(stage.participantCount.toDouble() / stage.groupCount).toInt()
if (stage.participantCount % stage.groupCount < group) {
min + 1
} else {
min
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentStageService.kt`
around lines 133 - 149, The bounds check in createRoundMatchesForGroupStageGroup
is inverted, so valid group numbers are rejected while out-of-range values may
pass. Update the require condition to fail only when the requested group exceeds
the stage’s groupCount, and keep the existing error message consistent with that
validation. Use the createRoundMatchesForGroupStageGroup function and the
stage.groupCount/group parameters to locate and correct the check.

</object>
<div th:if="${match.homeTeamScore!=null && match.awayTeamScore!=null}" th:remove="tag">
<label class="card-label">Gólok</label>
<p th:number="${match.homeTeamScore}" class="highlight">Home team goals</p>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Relevant template lines =="
sed -n '90,120p' backend/src/main/resources/templates/matchScore.html

echo
echo "== Search for th:number usage =="
rg -n --hidden --no-ignore-vcs 'th:number|number="' backend/src/main/resources || true

echo
echo "== Search for custom Thymeleaf dialect/processor references =="
rg -n --hidden --no-ignore-vcs 'Thymeleaf|IDialect|IProcessor|AttributeTagProcessor|number' backend/src/main java . || true

Repository: kir-dev/cmsch

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Template excerpt =="
sed -n '96,114p' backend/src/main/resources/templates/matchScore.html

echo
echo "== th:number occurrences in backend =="
rg -n --hidden --no-ignore-vcs 'th:number' backend || true

echo
echo "== Thymeleaf dependency/config in backend build files =="
rg -n --hidden --no-ignore-vcs 'thymeleaf|Thymeleaf|spring-boot-starter-thymeleaf|IDialect|IProcessor|AttributeTagProcessor' backend/build.gradle backend/pom.xml backend/settings.gradle backend/src/main || true

Repository: kir-dev/cmsch

Length of output: 8073


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Build files at repo root/backends =="
fd -a 'build.gradle*|pom.xml|settings.gradle*' . || true

echo
echo "== Thymeleaf-related classes in backend source =="
rg -n --hidden --no-ignore-vcs --glob 'backend/src/main/**' \
  'class .*Dialect|interface .*Dialect|extends .*Dialect|implements .*Dialect|AbstractAttributeTagProcessor|AttributeTagProcessor|IProcessor|IDialect|SpringTemplateEngine|TemplateEngine|thymeleaf' \
  backend/src/main || true

echo
echo "== matchScore template context =="
sed -n '1,140p' backend/src/main/resources/templates/matchScore.html

Repository: kir-dev/cmsch

Length of output: 14362


Replace th:number with th:text for the score values backend/src/main/resources/templates/matchScore.html:100,110
th:number isn’t handled here, so the home/away score values stay as placeholder text instead of rendering the actual numbers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/main/resources/templates/matchScore.html` at line 100, The score
fields in matchScore.html are using an unsupported Thymeleaf attribute, so the
values are not rendered. Update the home and away score elements in the
matchScore template to use th:text instead of th:number, keeping the existing
match.homeTeamScore and match.awayTeamScore bindings so the actual numbers
display correctly.

<td th:text="${team.teamName}">
csapatnév
</td>
<td th:if="${stageLevel>1}">todo</td>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

"todo" placeholder is visible to end users.

The "Előző eredmény" (Previous result) column shows literal todo text for all stages with stageLevel > 1. This is user-facing incomplete functionality that should either be implemented or hidden until ready.

💡 Suggested interim fix
-                <td th:if="${stageLevel>1}">todo</td>
+                <td th:if="${stageLevel>1}" th:text="${team.previousResult ?: '—'}">todo</td>

Or hide the column entirely until the feature is implemented:

-                <th th:if="${stageLevel>1}">Előző eredmény</th>
+                <!-- TODO: implement previous result display for stages > level 1 -->
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/main/resources/templates/seedSettings.html` at line 80, The seed
settings template currently renders a user-visible placeholder in the
stageLevel>1 branch of the table cell, so update the th:if/th:text logic in
seedSettings.html to remove the literal placeholder from the “Előző eredmény”
column. Either wire this cell to the real previous-result value in the relevant
template binding or hide the column/cell until the feature is implemented, using
the existing stageLevel condition as the locator.

Comment on lines +8 to +11
const getScoreColor = (match: MatchView, score1?: number, score2?: number) => {
if (match.status !== 'COMPLETED' || score1 === undefined || score2 === undefined) return 'gray.600'
return score1 > score2 ? 'green.600' : 'red.600'
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Chakra color tokens are invalid in inline styles — scores won't be colored.

getScoreColor returns Chakra UI semantic tokens ('gray.600', 'green.600', 'red.600'), but these are used in inline style={{ color: ... }} objects (lines 46, 59). Chakra tokens are only resolved when used via Chakra's color prop or useColorModeValue/theme hooks — not in raw inline styles. The browser will treat them as invalid CSS color values, so score colors won't render.

Use either Chakra's <Text color={...}> component or replace with actual CSS color values.

🐛 Proposed fix using CSS hex values
 const getScoreColor = (match: MatchView, score1?: number, score2?: number) => {
   if (match.status !== 'COMPLETED' || score1 === undefined || score2 === undefined) return '`#4a5568`'
-  return score1 > score2 ? 'green.600' : 'red.600'
+  return score1 > score2 ? '`#2f855a`' : '`#c53030`'
 }

Also applies to: 46-46, 59-59

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/tournament/components/Match.tsx` around lines 8 - 11,
getScoreColor in Match.tsx is returning Chakra semantic tokens that are then
passed into inline style color values, so the browser never resolves them.
Update the score rendering in Match to use Chakra’s color prop (or another
Chakra-aware styling path) instead of style={{ color: ... }}, or change
getScoreColor to return real CSS color values so the completed score text is
actually colored.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants