Initial commit: TSS Bot Web Frontend (React/Vite + production server)

This commit is contained in:
clxud
2026-07-02 02:09:34 +00:00
commit 36092f0269
87 changed files with 34597 additions and 0 deletions
+217
View File
@@ -0,0 +1,217 @@
#!/usr/bin/env bash
#
# verify_game_detail.sh — end-to-end check for the TSSBOT game-detail parity work.
#
# Spins up a SELF-CONTAINED fixture (temp SQLite DBs, vehicle caches, icons) and
# exercises the whole stack: the Python log builder, the Rust backend (dedup fix,
# vehicle translation, logs endpoint), and the Node prod server (icon serving +
# API proxy allowlist). Touches NO production data. Exits non-zero on any failure.
#
# Usage:
# bash scripts/verify_game_detail.sh
#
# Env overrides:
# BOTS_REPO path to the BOTS repo (default: ~/GitHub/BOTS, fallback ~/BOTS, /home/deploy/BOTS)
# PYTHON python interpreter (default: BOTS venv if present, else python3)
# BE_PORT backend port (default 6071)
# WEB_PORT node server port (default 3071)
set -uo pipefail
WEB_REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BE_PORT="${BE_PORT:-6071}"
WEB_PORT="${WEB_PORT:-3071}"
# ---- locate BOTS repo + python --------------------------------------------
if [[ -z "${BOTS_REPO:-}" ]]; then
for c in "$HOME/GitHub/BOTS" "$HOME/BOTS" "/home/deploy/BOTS"; do
[[ -d "$c" ]] && BOTS_REPO="$c" && break
done
fi
if [[ -z "${BOTS_REPO:-}" || ! -d "$BOTS_REPO" ]]; then
echo "FAIL: could not find BOTS repo; set BOTS_REPO=..." >&2
exit 1
fi
if [[ -z "${PYTHON:-}" ]]; then
if [[ -x "$BOTS_REPO/SHARED/.venv/bin/python" ]]; then
PYTHON="$BOTS_REPO/SHARED/.venv/bin/python"
else
PYTHON="python3"
fi
fi
PASS=0
FAIL=0
ok() { echo " PASS: $1"; PASS=$((PASS+1)); }
bad() { echo " FAIL: $1" >&2; FAIL=$((FAIL+1)); }
# assert_eq <label> <actual> <expected>
assert_eq() { if [[ "$2" == "$3" ]]; then ok "$1 ($2)"; else bad "$1: got '$2' expected '$3'"; fi; }
WD="$(mktemp -d)"
ICONS="$(mktemp -d)"
STORE="$(mktemp -d)"
BE_PID=""
WEB_PID=""
cleanup() {
[[ -n "$BE_PID" ]] && kill "$BE_PID" 2>/dev/null
[[ -n "$WEB_PID" ]] && kill "$WEB_PID" 2>/dev/null
rm -rf "$WD" "$ICONS" "$STORE"
}
trap cleanup EXIT
echo "== TSSBOT game-detail verification =="
echo "WEB_REPO=$WEB_REPO BOTS_REPO=$BOTS_REPO PYTHON=$PYTHON"
# ---------------------------------------------------------------------------
echo "== 1. Python log-builder unit tests =="
if "$PYTHON" "$BOTS_REPO/TSSBOT/tests/test_match_logs.py"; then ok "build_match_logs tests"; else bad "build_match_logs tests"; fi
# ---------------------------------------------------------------------------
echo "== 2. Backfill dry-run against a synthetic replay =="
SYN="$(mktemp -d)"
STORAGE_VOL_PATH="$SYN" "$PYTHON" - "$SYN" <<'PY'
import gzip, json, os, pathlib, sys
d = pathlib.Path(sys.argv[1]) / "REPLAYS" / "TSS" / "feed"
d.mkdir(parents=True)
game = {"_id": "feed", "winner": "[WIN]", "loser": "[LOS]",
"players": {"1": {"name": "a", "tag": "[WIN]", "team": "1",
"units": [{"unit": "ussr_t_34", "used": True}]}},
"chat": [{"uid": "1", "type": "ALL", "message": "gg", "time": 1000}],
"events": {"kills": []}}
gzip.open(d / "replay_data.json.gz", "wb").write(json.dumps(game).encode())
PY
if STORAGE_VOL_PATH="$SYN" "$PYTHON" "$BOTS_REPO/TSSBOT/scripts/backfill_match_logs.py" --dry-run | grep -q "Would backfill"; then
ok "backfill --dry-run runs"
else
bad "backfill --dry-run"
fi
rm -rf "$SYN"
# ---------------------------------------------------------------------------
echo "== 3. cargo tests + binary =="
if cargo test --manifest-path "$WEB_REPO/backend/Cargo.toml" >/tmp/vgd_cargo.log 2>&1; then ok "cargo test"; else bad "cargo test (see /tmp/vgd_cargo.log)"; fi
# `cargo test` only builds the test harness, not the standalone binary. Prefer the
# release binary (built at deploy); otherwise build a debug one.
BIN=""
if [[ -x "$WEB_REPO/backend/target/release/tssbot-backend" ]]; then
BIN="$WEB_REPO/backend/target/release/tssbot-backend"
ok "using release binary"
else
if cargo build --manifest-path "$WEB_REPO/backend/Cargo.toml" >/tmp/vgd_build.log 2>&1; then
BIN="$WEB_REPO/backend/target/debug/tssbot-backend"
ok "built debug binary"
else
bad "cargo build (see /tmp/vgd_build.log)"
fi
fi
[[ -n "$BIN" && -x "$BIN" ]] || bad "backend binary unavailable"
# ---------------------------------------------------------------------------
echo "== 4. Build fixture (multi-vehicle player to catch the double-count bug) =="
"$PYTHON" - "$WD" <<'PY'
import sqlite3, json, sys
wd = sys.argv[1]
b = sqlite3.connect(f"{wd}/tss_battles.db")
b.executescript("""
CREATE TABLE match_summary (session_id TEXT PRIMARY KEY, mission_mode TEXT, mission_name TEXT,
level_path TEXT, mission_path TEXT, difficulty TEXT, starttime_unix INT, endtime_unix INT,
duration REAL, draw INT DEFAULT 0, winning_slot TEXT, losing_slot TEXT, received_unix INT,
tournament_id INT, tournament_name TEXT, match_id TEXT, bracket TEXT);
CREATE TABLE player_games_hist (UID TEXT, nick TEXT, team_name TEXT, team_slot TEXT,
session_id TEXT, vehicle TEXT, vehicle_internal TEXT, ground_kills INT, air_kills INT,
assists INT, captures INT, deaths INT, score INT, missile_evades INT, shell_interceptions INT,
team_kills_stat INT, country_id INT, victor_bool TEXT, endtime_unix INT, team_id INT,
tss_role TEXT, pvp_ratio REAL);
CREATE TABLE match_logs (session_id TEXT PRIMARY KEY, chat_log_json TEXT, battle_log_json TEXT, event_log_json TEXT, built_unix INT);
""")
b.execute("INSERT INTO match_summary VALUES ('abc','Dom','Test Map','','','',0,1000,420.0,0,'1','2',0,0,'Cup Finals','','')")
# alice used TWO vehicles -> two rows, identical per-player stats (score 100).
b.execute("INSERT INTO player_games_hist VALUES ('1','alice','TeamWin','1','abc','t34','ussr_t_34',2,0,1,0,0,100,0,0,0,0,'Win',1000,10,'',1.5)")
b.execute("INSERT INTO player_games_hist VALUES ('1','alice','TeamWin','1','abc','is2','ussr_is_2',2,0,1,0,0,100,0,0,0,0,'Win',1000,10,'',1.5)")
b.execute("INSERT INTO player_games_hist VALUES ('2','bob','TeamLose','2','abc','pz','germ_pz_iv',0,0,0,0,1,10,0,0,0,0,'Loss',1000,11,'',0.5)")
b.execute("INSERT INTO match_logs VALUES ('abc', ?, ?, ?, 1000)",
(json.dumps(["[00:01] [ALL] [WIN] `alice`: gg"]),
json.dumps(["+[00:30] [WIN] alice (T-34) destroyed bob (Pz.IV)"]),
json.dumps({"kills": [{"offender_uid": "1", "offender_unit": "ussr_t_34", "offended_uid": "2", "offended_unit": "germ_pz_iv", "crashed": False, "time": 30000}], "damage": [], "chat": [{"uid": "1", "type": "ALL", "message": "gg", "time": 1000}]})))
b.commit()
t = sqlite3.connect(f"{wd}/tss_teams.db")
t.executescript("CREATE TABLE teams_data (team_id INT PRIMARY KEY, name TEXT, members INT DEFAULT 0, captain_uid TEXT);")
t.commit()
json.dump({"ussr_t_34": {"en": "T-34", "ru": "Т-34"}, "ussr_is_2": {"en": "IS-2"}}, open(f"{wd}/vt.json", "w"))
json.dump([["ussr_t_34", "T-34", "ussr_t_34.png", {}], ["ussr_is_2", "IS-2", "ussr_is_2.png", {}]], open(f"{wd}/vc.json", "w"))
print("fixture ready")
PY
printf '\x89PNG' > "$ICONS/ussr_t_34.png"
# ---------------------------------------------------------------------------
echo "== 5. Start backend + node server against the fixture =="
TSS_BATTLES_DB="$WD/tss_battles.db" TSS_TEAMS_DB="$WD/tss_teams.db" \
VEHICLE_TRANSLATIONS_JSON="$WD/vt.json" VEHICLE_DATA_CACHE_JSON="$WD/vc.json" \
BACKEND_PORT="$BE_PORT" "$BIN" >/tmp/vgd_be.log 2>&1 &
BE_PID=$!
# Override PUBLIC_ORIGIN so the server's same-origin guard accepts the localhost
# test origin. server.cjs's loadEnvFile only fills unset/empty vars, so these
# non-empty values win over the prod .env.
PORT="$WEB_PORT" API_UPSTREAM="http://127.0.0.1:$BE_PORT" VEHICLE_ICONS_DIR="$ICONS" \
UPTIME_STORAGE_DIR="$STORE" PUBLIC_ORIGIN="http://localhost:$WEB_PORT" \
node "$WEB_REPO/server.cjs" >/tmp/vgd_web.log 2>&1 &
WEB_PID=$!
# wait for both to listen
for _ in $(seq 1 20); do
curl -sf "localhost:$BE_PORT/health" >/dev/null 2>&1 && curl -sf "localhost:$WEB_PORT/health" >/dev/null 2>&1 && break
sleep 0.5
done
# ---------------------------------------------------------------------------
echo "== 6. Backend: dedup, vehicle translation, logs =="
GAME_EN="$(curl -s "localhost:$BE_PORT/api/tss/games/abc?lang=en")"
echo "$GAME_EN" | "$PYTHON" - <<PY
import json, sys
d = json.loads('''$GAME_EN''')
def field(name, actual, expected):
print((" PASS" if actual==expected else " FAIL")+f": {name}: {actual!r} (expected {expected!r})")
return actual==expected
alice = [pl for part in d["participants"] for pl in part["players"] if pl["uid"]=="1"][0]
win = [p for p in d["participants"] if p["team_name"]=="TeamWin"][0]
oks = []
oks.append(field("alice score deduped", alice["stats"]["score"], 100))
oks.append(field("alice vehicle count", len(alice["vehicles"]), 2))
oks.append(field("team total score", win["stats"]["score"], 100))
oks.append(field("game total score", d["game"]["stats"]["score"], 110))
oks.append(field("tournament_name", d["game"].get("tournament_name"), "Cup Finals"))
oks.append(field("duration", d["game"].get("duration"), 420.0))
oks.append(field("draw", d["game"]["draw"], False))
sys.exit(0 if all(oks) else 1)
PY
if [[ $? -eq 0 ]]; then ok "backend game detail (en)"; else bad "backend game detail (en)"; fi
RU_NAME="$(curl -s "localhost:$BE_PORT/api/tss/games/abc?lang=ru" | "$PYTHON" -c "import sys,json; d=json.load(sys.stdin); print([v['name'] for p in d['participants'] for pl in p['players'] for v in pl['vehicles'] if v['cdk']=='ussr_t_34'][0])")"
assert_eq "ru translation of ussr_t_34" "$RU_NAME" "Т-34"
LOG_COUNTS="$(curl -s "localhost:$BE_PORT/api/tss/games/abc/logs" | "$PYTHON" -c "import sys,json; d=json.load(sys.stdin); e=d.get('event_log', {}); print(len(d['chat_log']), len(d['battle_log']), len(e.get('kills', [])), len(e.get('chat', [])))")"
assert_eq "logs chat/battle/kill/raw-chat counts" "$LOG_COUNTS" "1 1 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")
assert_eq "icon served" "$(curl -s -o /dev/null -w '%{http_code}' "localhost:$WEB_PORT/vehicle-icons/ussr_t_34.png")" "200"
assert_eq "icon traversal blocked" "$(curl -s --path-as-is -o /dev/null -w '%{http_code}' "localhost:$WEB_PORT/vehicle-icons/..%2f..%2fserver.cjs")" "403"
assert_eq "proxy game?lang=en" "$(curl -s "${H[@]}" -o /dev/null -w '%{http_code}' "localhost:$WEB_PORT/api/tss/games/abc?lang=en")" "200"
assert_eq "proxy logs" "$(curl -s "${H[@]}" -o /dev/null -w '%{http_code}' "localhost:$WEB_PORT/api/tss/games/abc/logs")" "200"
assert_eq "proxy SPA shell" "$(curl -s -o /dev/null -w '%{http_code}' "localhost:$WEB_PORT/games/abc")" "200"
# bad param must NOT be 200 (allowlist rejects)
BADCODE="$(curl -s "${H[@]}" -o /dev/null -w '%{http_code}' "localhost:$WEB_PORT/api/tss/games/abc?evil=1")"
if [[ "$BADCODE" != "200" ]]; then ok "proxy blocks unknown param ($BADCODE)"; else bad "proxy allowed unknown param"; fi
# ---------------------------------------------------------------------------
echo
echo "== RESULT: $PASS passed, $FAIL failed =="
[[ $FAIL -eq 0 ]] || exit 1
echo "ALL CHECKS PASSED"