From 51c0dd264aaa0f50278fb8b93b1ec80a0a58d4bd Mon Sep 17 00:00:00 2001 From: FURRO404 Date: Thu, 18 Jun 2026 01:29:30 -0700 Subject: [PATCH] fix: dedup battle-logs list (one row/session), case-insensitive vehicle lookup, win/loss colors, clearer dividers, centered stat columns --- backend/src/main.rs | 107 +++++++++++++++------------------- frontend/src/App.jsx | 50 ++++++++-------- frontend/src/styles.css | 4 ++ scripts/verify_game_detail.sh | 4 ++ 4 files changed, 81 insertions(+), 84 deletions(-) diff --git a/backend/src/main.rs b/backend/src/main.rs index ab54b06..e75961a 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1322,6 +1322,9 @@ fn games_for(conn: &Connection, team_name: &str) -> Result, ApiErro } fn recent_games_for(conn: &Connection, limit: i64) -> Result, ApiError> { + // One row per SESSION (not per team) so the battle-logs list shows each game + // once. Both team names come from the winner/loser subqueries; player_count is + // the larger team's size so the "NvN" label is per-side. let mut stmt = conn .prepare( "WITH recent AS ( @@ -1332,47 +1335,25 @@ fn recent_games_for(conn: &Connection, limit: i64) -> Result, ApiEr ORDER BY timestamp DESC LIMIT ?1 ), - per_player AS ( - SELECT session_id, team_name, UID, - MAX(endtime_unix) AS endtime_unix, - MAX(CASE WHEN victor_bool = 'Win' THEN 1 ELSE 0 END) AS won, - MAX(ground_kills) AS ground_kills, - MAX(air_kills) AS air_kills, - MAX(assists) AS assists, - MAX(captures) AS captures, - MAX(deaths) AS deaths, - MAX(score) AS score, - MAX(missile_evades) AS missile_evades, - MAX(shell_interceptions) AS shell_interceptions, - MAX(team_kills_stat) AS team_kills_stat + team_size AS ( + SELECT session_id, team_name, COUNT(DISTINCT UID) AS players FROM player_games_hist WHERE session_id IN (SELECT session_id FROM recent) AND team_name IS NOT NULL AND team_name != '' - GROUP BY session_id, team_name COLLATE NOCASE, UID + GROUP BY session_id, team_name COLLATE NOCASE ) SELECT - pp.team_name, - pp.session_id, - COALESCE(m.endtime_unix, MAX(pp.endtime_unix), 0) AS timestamp, + r.session_id, + COALESCE(m.endtime_unix, r.timestamp, 0) AS timestamp, m.mission_name, m.mission_mode, m.tournament_name, m.duration, COALESCE(m.draw, 0), - CASE WHEN MAX(pp.won) = 1 THEN 'Win' ELSE 'Loss' END AS result, - COUNT(*), - COALESCE(SUM(pp.ground_kills), 0), - COALESCE(SUM(pp.air_kills), 0), - COALESCE(SUM(pp.assists), 0), - COALESCE(SUM(pp.captures), 0), - COALESCE(SUM(pp.deaths), 0), - COALESCE(SUM(pp.score), 0), - COALESCE(SUM(pp.missile_evades), 0), - COALESCE(SUM(pp.shell_interceptions), 0), - COALESCE(SUM(pp.team_kills_stat), 0), + (SELECT MAX(players) FROM team_size ts WHERE ts.session_id = r.session_id) AS player_count, (SELECT pg.team_name FROM player_games_hist pg - WHERE pg.session_id = pp.session_id + WHERE pg.session_id = r.session_id AND pg.team_name IS NOT NULL AND pg.team_name != '' AND pg.victor_bool = 'Win' @@ -1381,16 +1362,15 @@ fn recent_games_for(conn: &Connection, limit: i64) -> Result, ApiEr LIMIT 1), (SELECT pg.team_name FROM player_games_hist pg - WHERE pg.session_id = pp.session_id + WHERE pg.session_id = r.session_id AND pg.team_name IS NOT NULL AND pg.team_name != '' AND pg.victor_bool = 'Loss' GROUP BY pg.team_name COLLATE NOCASE ORDER BY COUNT(DISTINCT pg.UID) DESC, pg.team_name COLLATE NOCASE LIMIT 1) - FROM per_player pp - LEFT JOIN match_summary m ON m.session_id = pp.session_id - GROUP BY pp.team_name COLLATE NOCASE, pp.session_id + FROM recent r + LEFT JOIN match_summary m ON m.session_id = r.session_id ORDER BY timestamp DESC LIMIT ?1", ) @@ -1398,32 +1378,32 @@ fn recent_games_for(conn: &Connection, limit: i64) -> Result, ApiEr let rows = stmt .query_map(params![limit], |row| { - let timestamp: i64 = row.get(2)?; - let draw_int: i64 = row.get(7)?; + let timestamp: i64 = row.get(1)?; + let draw_int: i64 = row.get(6)?; Ok(GameRow { - team_name: row.get(0)?, - session_id: row.get(1)?, + team_name: None, + session_id: row.get(0)?, timestamp, endtime_unix: timestamp, - map_name: row.get(3)?, - mission_mode: row.get(4)?, - result: row.get(8)?, - player_count: row.get(9)?, - winning_team: row.get(19)?, - losing_team: row.get(20)?, - tournament_name: row.get(5)?, - duration: row.get(6)?, + map_name: row.get(2)?, + mission_mode: row.get(3)?, + result: String::new(), + player_count: row.get(7)?, + winning_team: row.get(8)?, + losing_team: row.get(9)?, + tournament_name: row.get(4)?, + duration: row.get(5)?, draw: draw_int != 0, stats: GameStats { - ground_kills: row.get(10)?, - air_kills: row.get(11)?, - assists: row.get(12)?, - captures: row.get(13)?, - deaths: row.get(14)?, - score: row.get(15)?, - missile_evades: row.get(16)?, - shell_interceptions: row.get(17)?, - team_kills_stat: row.get(18)?, + ground_kills: 0, + air_kills: 0, + assists: 0, + captures: 0, + deaths: 0, + score: 0, + missile_evades: 0, + shell_interceptions: 0, + team_kills_stat: 0, }, }) }) @@ -1796,11 +1776,18 @@ fn allowed_origins() -> AllowOrigin { } } +// Vehicle cdk keys are lowercased on load and at lookup time so DB casing +// (e.g. "us_M4A2_76W_sherman" vs "us_m4a2_76w_sherman") never misses, mirroring +// the Python LangTableReader's case-insensitive behaviour. fn load_vehicle_names(path: &FsPath) -> HashMap> { - match fs::read_to_string(path) { + let parsed: HashMap> = match fs::read_to_string(path) { Ok(s) => serde_json::from_str(&s).unwrap_or_default(), Err(_) => HashMap::new(), - } + }; + parsed + .into_iter() + .map(|(k, v)| (k.to_lowercase(), v)) + .collect() } fn load_vehicle_icons(path: &FsPath) -> HashMap { @@ -1815,7 +1802,7 @@ fn load_vehicle_icons(path: &FsPath) -> HashMap { entry.get(0).and_then(|v| v.as_str()), entry.get(2).and_then(|v| v.as_str()), ) { - out.insert(cdk.to_string(), icon.to_string()); + out.insert(cdk.to_lowercase(), icon.to_string()); } } out @@ -1826,7 +1813,7 @@ fn lookup_vehicle_name( cdk: &str, lang: &str, ) -> String { - if let Some(by_lang) = names.get(cdk) { + if let Some(by_lang) = names.get(&cdk.to_lowercase()) { if let Some(n) = by_lang.get(lang) { return n.clone(); } @@ -1839,9 +1826,9 @@ fn lookup_vehicle_name( fn lookup_vehicle_icon(icons: &HashMap, cdk: &str) -> String { icons - .get(cdk) + .get(&cdk.to_lowercase()) .cloned() - .unwrap_or_else(|| format!("{cdk}.png")) + .unwrap_or_else(|| format!("{}.png", cdk.to_lowercase())) } fn resolve_db_path(env_key: &str, default_file: &str) -> PathBuf { diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index b1cf7d8..1f98199 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -2870,43 +2870,45 @@ function GamePage({ gameId, navigate }) {
{participants.length ? ( -
+

Team / player

-

Air

-

Ground

-

Assists

-

Deaths

-

Caps

-

Score

+

Air

+

Ground

+

Assists

+

Deaths

+

Caps

+

Score

) : null} {participants.map((participant) => { const won = String(participant.result || '').toLowerCase() === 'win' + const accent = won ? 'border-l-win' : 'border-l-loss' + const nameColor = won ? 'text-win' : 'text-loss' return ( -
+
-
+
{(participant.players || []).map((player) => (
-

{formatNumber(player.stats?.air_kills)}

-

{formatNumber(player.stats?.ground_kills)}

-

{formatNumber(player.stats?.assists)}

-

{formatNumber(player.stats?.deaths)}

-

{formatNumber(player.stats?.captures)}

-

{formatNumber(player.stats?.score)}

+

{formatNumber(player.stats?.air_kills)}

+

{formatNumber(player.stats?.ground_kills)}

+

{formatNumber(player.stats?.assists)}

+

{formatNumber(player.stats?.deaths)}

+

{formatNumber(player.stats?.captures)}

+

{formatNumber(player.stats?.score)}

))}
diff --git a/frontend/src/styles.css b/frontend/src/styles.css index dd3218a..6bf7e15 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -80,6 +80,8 @@ --color-success: #00f2ff; --color-warning: #f4ee3e; --color-danger: #e82517; + --color-win: #1a9e4b; + --color-loss: #e82517; } :root[data-theme="dark"] { @@ -102,6 +104,8 @@ --color-success: #58f0f5; --color-warning: #f4ee3e; --color-danger: #ff6a5f; + --color-win: #46d17e; + --color-loss: #ff6a5f; color-scheme: dark; } diff --git a/scripts/verify_game_detail.sh b/scripts/verify_game_detail.sh index d6a1f34..654c70e 100755 --- a/scripts/verify_game_detail.sh +++ b/scripts/verify_game_detail.sh @@ -193,6 +193,10 @@ assert_eq "logs chat/battle counts" "$LOG_COUNTS" "1 1" MISS="$(curl -s "localhost:$BE_PORT/api/tss/games/deadbeef/logs" | "$PYTHON" -c "import sys,json; d=json.load(sys.stdin); print(len(d['chat_log'])+len(d['battle_log']))")" assert_eq "missing-session logs empty" "$MISS" "0" +# recent games must list each session ONCE (not once per team) +REC="$(curl -s "localhost:$BE_PORT/api/tss/games/recent?limit=50" | "$PYTHON" -c "import sys,json; d=json.load(sys.stdin); ids=[m['session_id'] for m in d['matches']]; print(ids.count('abc'), ([m['player_count'] for m in d['matches'] if m['session_id']=='abc'] or [0])[0])")" +assert_eq "recent lists session once + per-side count" "$REC" "1 1" + # --------------------------------------------------------------------------- echo "== 7. Node prod server: icons + proxy allowlist ==" H=(-H "Origin: http://localhost:$WEB_PORT" -H "Referer: http://localhost:$WEB_PORT/games/abc" -H "Sec-Fetch-Site: same-origin")