From 65ad10ad11c1cc3fa776bd573584150105e79dc7 Mon Sep 17 00:00:00 2001 From: deploy Date: Mon, 8 Jun 2026 01:07:57 +0000 Subject: [PATCH] align backend with TSS schema: drop SRE-specific fields, fix cross-tournament queries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove teams_points, clanrating, tag/short/long name, description, region, guild_id, clan_id — none of these exist in the TSS DB schema - Rename long_name → name throughout (TSS teams have one name, not long+short) - Cross-tournament stat queries now use team_name (string) from player_games_hist instead of team_id, since team_id is assigned per-tournament by Spectra - Leaderboard deduplicates teams_data by name with GROUP BY, MAX(team_id) for roster ref - team_members roster still uses team_id (correct within a single tournament) - Fix player_teams_for: was grouping by non-existent team_tag column, now team_id - Fix games_for: winning_team/losing_team → winning_slot/losing_slot; add mission_name - Remove joined_unix, points, sqb_points from PlayerSummary; nick resolved from battles_db - Remove rating_hourly from HistoryResponse (teams_points never existed) Co-Authored-By: Claude Sonnet 4.6 --- backend/src/main.rs | 280 ++++++++++++++------------------------------ 1 file changed, 85 insertions(+), 195 deletions(-) diff --git a/backend/src/main.rs b/backend/src/main.rs index 83155b2..3ae0b08 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -105,51 +105,34 @@ struct SearchResponse { #[derive(Serialize)] struct ResolveResponse { team_id: i64, - long_name: String, - tag_name: Option, name: String, } #[derive(Serialize)] struct TeamSearchRow { team_id: i64, - long_name: String, - tag_name: Option, + name: String, members: i64, - clanrating: Option, } #[derive(Serialize)] struct TeamLeaderboardRow { team_id: i64, - clan_id: i64, - long_name: String, - tag_name: Option, - short_name: Option, + name: String, player_count: i64, total_battles: i64, wins: i64, losses: i64, win_rate: f64, total_kills: i64, - points: Points, } #[derive(Serialize)] struct TeamDetail { team_id: i64, - clan_id: i64, - long_name: String, - tag_name: Option, - short_name: Option, - description: Option, - region: Option, + name: String, members: i64, captain_uid: Option, - guild_id: Option, - created_unix: Option, - updated_unix: Option, - clanrating: Option, data_set: &'static str, team_summary: TeamSummary, players: Vec, @@ -164,12 +147,6 @@ struct TeamSummary { win_rate: f64, kdr: f64, total_kills: i64, - points: Points, - total_points: i64, -} - -#[derive(Serialize)] -struct Points { total_points: i64, } @@ -178,9 +155,6 @@ struct PlayerSummary { uid: String, nick: Option, role: String, - joined_unix: Option, - points: i64, - sqb_points: i64, total_battles: i64, wins: i64, losses: i64, @@ -196,10 +170,8 @@ struct PlayerSummary { #[derive(Serialize)] struct HistoryResponse { team_id: i64, - long_name: String, - tag_name: Option, + name: String, history: Vec, - rating_hourly: Vec, } #[derive(Serialize)] @@ -211,17 +183,10 @@ struct PeriodHistory { win_rate: f64, } -#[derive(Serialize)] -struct RatingPoint { - timestamp: i64, - rating: i64, -} - #[derive(Serialize)] struct GamesResponse { team_id: i64, - long_name: String, - tag_name: Option, + name: String, games: Vec, } @@ -290,7 +255,6 @@ struct PlayerCareer { #[derive(Serialize)] struct PlayerTeamRef { team_id: Option, - team_tag: Option, team_name: Option, games: i64, last_seen: i64, @@ -298,16 +262,9 @@ struct PlayerTeamRef { struct TeamRecord { team_id: i64, - long_name: String, - tag_name: Option, - description: Option, - region: Option, + name: String, members: i64, captain_uid: Option, - guild_id: Option, - created_unix: Option, - updated_unix: Option, - clanrating: Option, } #[tokio::main] @@ -411,12 +368,14 @@ async fn leaderboard( let limit = i64::from(query.limit.unwrap_or(100).clamp(1, 100)); let teams_conn = open_db(&state.teams_db)?; let battles_conn = open_db(&state.battles_db)?; + // Deduplicate teams by name across tournaments — pick the highest team_id + // (most recent) per name for the roster count, but stats come from team_name. let mut stmt = teams_conn .prepare( - "SELECT team_id, long_name, tag_name, description, region, members, captain_uid, guild_id, - created_unix, updated_unix, clanrating + "SELECT MAX(team_id), name, MAX(members), MAX(captain_uid) FROM teams_data - ORDER BY COALESCE(clanrating, 0) DESC, members DESC, long_name COLLATE NOCASE ASC + GROUP BY name COLLATE NOCASE + ORDER BY MAX(members) DESC, name COLLATE NOCASE ASC LIMIT ?1", ) .map_err(db_error)?; @@ -429,22 +388,16 @@ async fn leaderboard( let mut rows = Vec::with_capacity(teams.len()); for team in teams { - let summary = team_summary_for(&battles_conn, team.team_id)?; + let summary = team_summary_for(&battles_conn, &team.name)?; rows.push(TeamLeaderboardRow { team_id: team.team_id, - clan_id: team.team_id, - long_name: team.long_name.clone(), - tag_name: team.tag_name.clone(), - short_name: team.tag_name.clone(), + name: team.name, player_count: team.members, total_battles: summary.total_battles, wins: summary.wins, losses: summary.losses, win_rate: summary.win_rate, total_kills: summary.total_kills, - points: Points { - total_points: team.clanrating.unwrap_or(summary.total_points), - }, }); } @@ -460,9 +413,7 @@ async fn resolve_team( let team = find_team(&conn, name)?.ok_or_else(|| ApiError::not_found("Team not found"))?; Ok(Json(ResolveResponse { team_id: team.team_id, - long_name: team.long_name.clone(), - tag_name: team.tag_name.clone(), - name: team.tag_name.clone().unwrap_or(team.long_name), + name: team.name, })) } @@ -477,17 +428,13 @@ async fn search_teams( let conn = open_db(&state.teams_db)?; let mut stmt = conn .prepare( - "SELECT team_id, long_name, tag_name, members, clanrating + "SELECT team_id, name, members FROM teams_data - WHERE long_name LIKE ?1 ESCAPE '\\' OR tag_name LIKE ?1 ESCAPE '\\' + WHERE name LIKE ?1 ESCAPE '\\' ORDER BY - CASE - WHEN tag_name = ?2 COLLATE NOCASE THEN 0 - WHEN long_name = ?2 COLLATE NOCASE THEN 1 - ELSE 2 - END, - COALESCE(clanrating, 0) DESC, - long_name COLLATE NOCASE ASC + CASE WHEN name = ?2 COLLATE NOCASE THEN 0 ELSE 1 END, + members DESC, + name COLLATE NOCASE ASC LIMIT ?3", ) .map_err(db_error)?; @@ -496,10 +443,8 @@ async fn search_teams( .query_map(params![like, name, limit], |row| { Ok(TeamSearchRow { team_id: row.get(0)?, - long_name: row.get(1)?, - tag_name: row.get(2)?, - members: row.get(3)?, - clanrating: row.get(4)?, + name: row.get(1)?, + members: row.get(2)?, }) }) .map_err(db_error)? @@ -518,23 +463,15 @@ async fn team_detail( let battles_conn = open_db(&state.battles_db)?; let team = find_team(&teams_conn, &decoded)?.ok_or_else(|| ApiError::not_found("Team not found"))?; - let summary = team_summary_for(&battles_conn, team.team_id)?; - let players = player_summaries_for(&teams_conn, &battles_conn, team.team_id)?; + let summary = team_summary_for(&battles_conn, &team.name)?; + // team_id is the most recent tournament entry — used only for the roster lookup. + let players = player_summaries_for(&teams_conn, &battles_conn, team.team_id, &team.name)?; Ok(Json(TeamDetail { team_id: team.team_id, - clan_id: team.team_id, - long_name: team.long_name, - tag_name: team.tag_name.clone(), - short_name: team.tag_name, - description: team.description, - region: team.region, + name: team.name, members: team.members, captain_uid: team.captain_uid, - guild_id: team.guild_id, - created_unix: team.created_unix, - updated_unix: team.updated_unix, - clanrating: team.clanrating, data_set: "tss", team_summary: summary, players, @@ -551,15 +488,12 @@ async fn team_history( let team = find_team(&teams_conn, &decoded)?.ok_or_else(|| ApiError::not_found("Team not found"))?; - let history = period_history_for(&battles_conn, team.team_id)?; - let rating_hourly = rating_history_for(&teams_conn, team.team_id)?; + let history = period_history_for(&battles_conn, &team.name)?; Ok(Json(HistoryResponse { team_id: team.team_id, - long_name: team.long_name, - tag_name: team.tag_name, + name: team.name, history, - rating_hourly, })) } @@ -572,12 +506,11 @@ async fn team_games( let battles_conn = open_db(&state.battles_db)?; let team = find_team(&teams_conn, &decoded)?.ok_or_else(|| ApiError::not_found("Team not found"))?; - let games = games_for(&battles_conn, team.team_id)?; + let games = games_for(&battles_conn, &team.name)?; Ok(Json(GamesResponse { team_id: team.team_id, - long_name: team.long_name, - tag_name: team.tag_name, + name: team.name, games, })) } @@ -639,33 +572,19 @@ fn db_error(error: rusqlite::Error) -> ApiError { fn read_team_record(row: &rusqlite::Row<'_>) -> rusqlite::Result { Ok(TeamRecord { team_id: row.get(0)?, - long_name: row.get(1)?, - tag_name: row.get(2)?, - description: row.get(3)?, - region: row.get(4)?, - members: row.get(5)?, - captain_uid: row.get(6)?, - guild_id: row.get(7)?, - created_unix: row.get(8)?, - updated_unix: row.get(9)?, - clanrating: row.get(10)?, + name: row.get(1)?, + members: row.get(2)?, + captain_uid: row.get(3)?, }) } fn find_team(conn: &Connection, name: &str) -> Result, ApiError> { + // Return the most recent tournament entry for this team (highest team_id). conn.query_row( - "SELECT team_id, long_name, tag_name, description, region, members, captain_uid, guild_id, - created_unix, updated_unix, clanrating + "SELECT team_id, name, members, captain_uid FROM teams_data - WHERE team_id = ?1 - OR long_name = ?2 COLLATE NOCASE - OR tag_name = ?2 COLLATE NOCASE - ORDER BY - CASE - WHEN tag_name = ?2 COLLATE NOCASE THEN 0 - WHEN long_name = ?2 COLLATE NOCASE THEN 1 - ELSE 2 - END + WHERE name = ?2 COLLATE NOCASE OR team_id = ?1 + ORDER BY team_id DESC LIMIT 1", params![name.parse::().ok(), name], read_team_record, @@ -674,7 +593,7 @@ fn find_team(conn: &Connection, name: &str) -> Result, ApiErr .map_err(db_error) } -fn team_summary_for(conn: &Connection, team_id: i64) -> Result { +fn team_summary_for(conn: &Connection, team_name: &str) -> Result { conn.query_row( "SELECT COUNT(DISTINCT session_id), @@ -687,8 +606,8 @@ fn team_summary_for(conn: &Connection, team_id: i64) -> Result Result Result Result, ApiError> { let mut stmt = teams_conn .prepare( - "SELECT uid, nick, role, joined_unix, points + "SELECT uid, role FROM team_members WHERE team_id = ?1 - ORDER BY points DESC, nick COLLATE NOCASE ASC", + ORDER BY uid", ) .map_err(db_error)?; let members = stmt .query_map(params![team_id], |row| { Ok(( row.get::<_, String>(0)?, - row.get::<_, Option>(1)?, - row.get::<_, String>(2)?, - row.get::<_, Option>(3)?, - row.get::<_, i64>(4)?, + row.get::<_, String>(1)?, )) }) .map_err(db_error)? @@ -756,15 +672,17 @@ fn player_summaries_for( COALESCE(SUM(air_kills), 0), COALESCE(SUM(assists), 0), COALESCE(SUM(deaths), 0), - COALESCE(SUM(score), 0) + (SELECT nick FROM player_games_hist + WHERE UID = ?2 AND nick IS NOT NULL + ORDER BY endtime_unix DESC LIMIT 1) FROM player_games_hist - WHERE team_id = ?1 AND UID = ?2", + WHERE team_name = ?1 COLLATE NOCASE AND UID = ?2", ) .map_err(db_error)?; - for (uid, nick, role, joined_unix, points) in members { + for (uid, role) in members { let summary = stats_stmt - .query_row(params![team_id, uid], |row| { + .query_row(params![team_name, uid], |row| { let battles: i64 = row.get(0)?; let wins: i64 = row.get(1)?; let losses: i64 = row.get(2)?; @@ -772,19 +690,12 @@ fn player_summaries_for( let air: i64 = row.get(4)?; let assists: i64 = row.get(5)?; let deaths: i64 = row.get(6)?; - let score: i64 = row.get(7)?; + let nick: Option = row.get(7)?; let total_kills = ground + air; Ok(PlayerSummary { uid: uid.clone(), - nick: nick.clone(), + nick, role: role.clone(), - joined_unix, - points, - sqb_points: if points == 0 { - score + assists + total_kills - } else { - points - }, total_battles: battles, wins, losses, @@ -801,6 +712,7 @@ fn player_summaries_for( out.push(summary); } + out.sort_by(|a, b| b.total_battles.cmp(&a.total_battles)); Ok(out) } @@ -931,22 +843,21 @@ fn player_career_for(conn: &Connection, uid: &str) -> Result Result, ApiError> { let mut stmt = conn .prepare( - "SELECT team_tag, MAX(team_name) AS team_name, team_id, + "SELECT team_id, MAX(team_name) AS team_name, COUNT(DISTINCT session_id) AS games, MAX(endtime_unix) AS last_seen FROM player_games_hist - WHERE UID = ?1 - GROUP BY team_tag + WHERE UID = ?1 AND team_id IS NOT NULL + GROUP BY team_id ORDER BY last_seen DESC", ) .map_err(db_error)?; let teams = stmt .query_map(params![uid], |row| { Ok(PlayerTeamRef { - team_tag: row.get(0)?, + team_id: row.get(0)?, team_name: row.get(1)?, - team_id: row.get(2)?, - games: row.get(3)?, - last_seen: row.get(4)?, + games: row.get(2)?, + last_seen: row.get(3)?, }) }) .map_err(db_error)? @@ -955,7 +866,7 @@ fn player_teams_for(conn: &Connection, uid: &str) -> Result, Ok(teams) } -fn period_history_for(conn: &Connection, team_id: i64) -> Result, ApiError> { +fn period_history_for(conn: &Connection, team_name: &str) -> Result, ApiError> { let mut stmt = conn .prepare( "SELECT @@ -964,14 +875,14 @@ fn period_history_for(conn: &Connection, team_id: i64) -> Result 0 + WHERE team_name = ?1 COLLATE NOCASE AND endtime_unix > 0 GROUP BY period ORDER BY period ASC", ) .map_err(db_error)?; let rows = stmt - .query_map(params![team_id], |row| { + .query_map(params![team_name], |row| { let period: String = row.get(0)?; let battles: i64 = row.get(1)?; let wins: i64 = row.get(2)?; @@ -990,35 +901,13 @@ fn period_history_for(conn: &Connection, team_id: i64) -> Result Result, ApiError> { - let mut stmt = conn - .prepare( - "SELECT unix_time, COALESCE(total_score, 0) - FROM teams_points - WHERE team_id = ?1 - ORDER BY unix_time ASC", - ) - .map_err(db_error)?; - - let rows = stmt - .query_map(params![team_id], |row| { - Ok(RatingPoint { - timestamp: row.get(0)?, - rating: row.get(1)?, - }) - }) - .map_err(db_error)? - .collect::, _>>() - .map_err(db_error)?; - Ok(rows) -} - -fn games_for(conn: &Connection, team_id: i64) -> Result, ApiError> { +fn games_for(conn: &Connection, team_name: &str) -> Result, ApiError> { let mut stmt = conn .prepare( "SELECT p.session_id, COALESCE(m.endtime_unix, MAX(p.endtime_unix), 0) AS timestamp, + m.mission_name, m.mission_mode, CASE WHEN MAX(CASE WHEN p.victor_bool = 'Win' THEN 1 ELSE 0 END) = 1 THEN 'Win' @@ -1034,11 +923,11 @@ fn games_for(conn: &Connection, team_id: i64) -> Result, ApiError> COALESCE(SUM(p.missile_evades), 0), COALESCE(SUM(p.shell_interceptions), 0), COALESCE(SUM(p.team_kills_stat), 0), - m.winning_team, - m.losing_team + m.winning_slot, + m.losing_slot FROM player_games_hist p LEFT JOIN match_summary m ON m.session_id = p.session_id - WHERE p.team_id = ?1 + WHERE p.team_name = ?1 COLLATE NOCASE GROUP BY p.session_id ORDER BY timestamp DESC LIMIT 100", @@ -1046,30 +935,31 @@ fn games_for(conn: &Connection, team_id: i64) -> Result, ApiError> .map_err(db_error)?; let rows = stmt - .query_map(params![team_id], |row| { + .query_map(params![team_name], |row| { let session_id: String = row.get(0)?; let timestamp: i64 = row.get(1)?; - let mission_mode: Option = row.get(2)?; + let map_name: Option = row.get(2)?; + let mission_mode: Option = row.get(3)?; Ok(GameRow { session_id, timestamp, endtime_unix: timestamp, - map_name: mission_mode.clone(), + map_name, mission_mode, - result: row.get(3)?, - player_count: row.get(4)?, - winning_team: row.get(14)?, - losing_team: row.get(15)?, + result: row.get(4)?, + player_count: row.get(5)?, + winning_team: row.get(15)?, + losing_team: row.get(16)?, stats: GameStats { - ground_kills: row.get(5)?, - air_kills: row.get(6)?, - assists: row.get(7)?, - captures: row.get(8)?, - deaths: row.get(9)?, - score: row.get(10)?, - missile_evades: row.get(11)?, - shell_interceptions: row.get(12)?, - team_kills_stat: row.get(13)?, + ground_kills: row.get(6)?, + air_kills: row.get(7)?, + assists: row.get(8)?, + captures: row.get(9)?, + deaths: row.get(10)?, + score: row.get(11)?, + missile_evades: row.get(12)?, + shell_interceptions: row.get(13)?, + team_kills_stat: row.get(14)?, }, }) })