diff --git a/backend/README.md b/backend/README.md index 760f811..5ba5a19 100644 --- a/backend/README.md +++ b/backend/README.md @@ -6,6 +6,10 @@ It reads two SQLite databases: - `TSS_BATTLES_DB` for `tss_battles.db` (matches, players, and the `match_logs` table) - `TSS_TEAMS_DB` for `tss_teams.db` +- `TSS_TOURNAMENTS_DB` for `tss_tournaments.db` + +If any of these are unset, the backend first looks under `STORAGE_VOL_PATH` +before falling back to the current working directory. - `BACKEND_HOST` bind host, default `127.0.0.1` - `BACKEND_ALLOWED_ORIGINS` comma-separated browser origins allowed by CORS diff --git a/backend/src/main.rs b/backend/src/main.rs index a18704a..ea346f1 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -2299,9 +2299,13 @@ fn lookup_vehicle_icon(icons: &HashMap, cdk: &str) -> String { } fn resolve_db_path(env_key: &str, default_file: &str) -> PathBuf { - let raw = env::var(env_key).unwrap_or_else(|_| default_file.to_string()); - let expanded = expand_home(&raw); - let path = PathBuf::from(expanded); + let path = if let Ok(raw) = env::var(env_key) { + PathBuf::from(expand_home(&raw)) + } else if let Ok(storage) = env::var("STORAGE_VOL_PATH") { + PathBuf::from(expand_home(&storage)).join(default_file) + } else { + PathBuf::from(default_file) + }; if path.is_absolute() { path } else { diff --git a/example.env b/example.env index 6a9e844..e588dbe 100644 --- a/example.env +++ b/example.env @@ -10,6 +10,7 @@ BACKEND_HOST=127.0.0.1 BACKEND_ALLOWED_ORIGINS=https://example.com TSS_BATTLES_DB=./tss_battles.db TSS_TEAMS_DB=./tss_teams.db +TSS_TOURNAMENTS_DB=./tss_tournaments.db # Vehicle name translation + icon caches (shared STORAGE/CACHE, built by the bots). # The backend loads these at startup to translate vehicle_internal (cdk) -> name diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index da4be24..d543d17 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -3714,10 +3714,11 @@ function tournamentStatusLabel(status) { return raw.charAt(0).toUpperCase() + raw.slice(1) } -function sideFromMatch(match) { +function sideFromMatch(match, context = {}) { + const bracket = String(match?.type_bracket || '').toLowerCase() + if (context.hasLoserSide && bracket.includes('semifinal')) return 'loser' const side = String(match?.side || '').toLowerCase() if (side) return side - const bracket = String(match?.type_bracket || '').toLowerCase() if (bracket.includes('swiss')) return 'swiss' if (bracket.includes('group')) return 'group' if (bracket.includes('looser') || bracket.includes('loser')) return 'loser' @@ -3813,8 +3814,15 @@ function TournamentsPage({ navigate }) { function groupMatchesBySide(matches) { const bySide = new Map() + const context = { + hasLoserSide: matches.some((match) => { + const bracket = String(match?.type_bracket || '').toLowerCase() + const side = String(match?.side || '').toLowerCase() + return bracket.includes('looser') || bracket.includes('loser') || side === 'loser' + }), + } matches.forEach((match) => { - const side = sideFromMatch(match) + const side = sideFromMatch(match, context) if (!bySide.has(side)) bySide.set(side, []) bySide.get(side).push(match) }) @@ -3862,7 +3870,7 @@ function TournamentMatchCard({ match, navigate }) { const bWon = winner && teamB && winner === teamB.toLowerCase() const battles = Array.isArray(match.battles) ? match.battles : [] - const teamRow = (name, score, won) => ( + const teamRow = (name, score, won, emptyLabel = 'TBD') => (
{name ? (
) + const emptyLabel = match.status === 'bye' ? 'BYE' : 'TBD' return (
- {teamRow(teamA, match.score_a, aWon)} -
{teamRow(teamB, match.score_b, bWon)}
+ {teamRow(teamA, match.score_a, aWon, emptyLabel)} +
{teamRow(teamB, match.score_b, bWon, emptyLabel)}
{tournamentStatusLabel(match.status)} {match.position !== null && match.position !== undefined ? Slot {Number(match.position) + 1} : null} @@ -3923,19 +3932,27 @@ function TournamentBracketSide({ side, navigate }) {

{side.label}

-
+
{rounds.map((round, roundIndex) => ( -
+

{roundLabel(side.raw, round.round, roundIndex, rounds.length)}

{round.matches.map((match) => ( - 0 ? 'tournament-match-node-left' : '', + roundIndex < rounds.length - 1 ? 'tournament-match-node-right' : '', + ].filter(Boolean).join(' ')} key={`${match.type_bracket}-${match.match_id}`} - match={match} - navigate={navigate} - /> + > + +
))}
diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 5068669..f6a07e1 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -831,6 +831,47 @@ h3 { font-size: 0.62rem; } +.tournament-bracket-grid { + display: flex; + gap: 2.6rem; + min-width: max-content; +} + +.tournament-round-column { + position: relative; + display: flex; + width: 190px; + min-width: 190px; + flex-direction: column; + gap: 0.75rem; +} + +.tournament-match-node { + position: relative; + z-index: 1; +} + +.tournament-match-node::before, +.tournament-match-node::after { + position: absolute; + top: 50%; + z-index: 0; + display: block; + height: 1px; + width: 1.3rem; + background: color-mix(in srgb, var(--color-border) 78%, var(--color-fury-violet)); + content: ""; + pointer-events: none; +} + +.tournament-match-node-left::before { + right: calc(100% + 0.05rem); +} + +.tournament-match-node-right::after { + left: calc(100% + 0.05rem); +} + @keyframes scrollPulse { 0% { transform: translateY(-100%);