diff --git a/CricketGame/backend/realtime/match/match_persistence.py b/CricketGame/backend/realtime/match/match_persistence.py index a369187..e5d463d 100644 --- a/CricketGame/backend/realtime/match/match_persistence.py +++ b/CricketGame/backend/realtime/match/match_persistence.py @@ -83,6 +83,7 @@ def save_match_history(manager, room, match: Match, potm_data: dict, tournament_ match_id=match.id, room_code=room.code, mode=match.mode, + timestamp=datetime.utcnow(), side_a=json.dumps(match.side_a), side_b=json.dumps(match.side_b), scorecard_1=json.dumps(sc1), diff --git a/CricketGame/backend/realtime/tournament.py b/CricketGame/backend/realtime/tournament.py index 3c3d686..af7124b 100644 --- a/CricketGame/backend/realtime/tournament.py +++ b/CricketGame/backend/realtime/tournament.py @@ -187,11 +187,13 @@ def apply_tournament_cancellation(manager, room, match: Match) -> dict: def save_tournament_history(manager, room, t: Tournament, awards: dict) -> None: + from datetime import datetime db = SessionLocal() try: history = TournamentHistory( tournament_id=room.tournament_id, room_code=room.code, + timestamp=datetime.utcnow(), players=json.dumps(t.players), standings=json.dumps(t.get_sorted_standings()), playoff_bracket=json.dumps({ diff --git a/CricketGame/fix_tournament_timestamps.py b/CricketGame/fix_tournament_timestamps.py new file mode 100644 index 0000000..897f89c --- /dev/null +++ b/CricketGame/fix_tournament_timestamps.py @@ -0,0 +1,157 @@ +""" +Tournament Timestamp Fix Script +================================ +Fixes tournaments with 01/01/1970 timestamps or NULL timestamps. +Also updates specific problematic tournament IDs. + +HOW TO RUN (on AlwaysData server console): + 1. Navigate to the CricketGame directory: + cd ~/www/CricketGame + 2. Run: + python fix_tournament_timestamps.py + +NOTES: + - Tournaments with epoch (01/01/1970) or NULL timestamps are updated with current datetime + - Specific tournament IDs (c6e5, 64c5, 8247, 369c, 1738, 088b) are targeted for fixing +""" + +import os +import sys +from datetime import datetime +from sqlalchemy import ( + create_engine, Column, Integer, String, Text, DateTime, ForeignKey, inspect, text +) +from sqlalchemy.orm import sessionmaker, DeclarativeBase + +# ── Locate database ────────────────────────────────────────────────────────────── + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) + +# DB path: respect DATABASE_URL env var (same as backend config.py does) +DATABASE_URL = os.environ.get("DATABASE_URL") +if not DATABASE_URL: + # Try common locations + candidates = [ + os.path.join(SCRIPT_DIR, "backend", "cricket.db"), + os.path.join(SCRIPT_DIR, "cricket.db"), + os.path.join(os.path.dirname(SCRIPT_DIR), "cricket.db"), + ] + db_file = next((p for p in candidates if os.path.exists(p)), None) + if db_file: + DATABASE_URL = f"sqlite:///{db_file}" + else: + # Default: use next to this script (same as running uvicorn from here) + DATABASE_URL = f"sqlite:///{os.path.join(SCRIPT_DIR, 'cricket.db')}" + +print(f"Using database: {DATABASE_URL}") +print() + +# ── Minimal ORM (mirrors backend models) ───────────────────────────────────────── + +engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) +Session = sessionmaker(bind=engine) + +class Base(DeclarativeBase): + pass + +class TournamentHistory(Base): + __tablename__ = "tournament_history" + id = Column(Integer, primary_key=True) + tournament_id = Column(String(20), unique=True, nullable=False, index=True) + room_code = Column(String(20), nullable=False) + timestamp = Column(DateTime) + players = Column(Text, nullable=False) + standings = Column(Text, nullable=False) + playoff_bracket = Column(Text, nullable=True) + playoff_results = Column(Text, nullable=True) + match_ids = Column(Text, nullable=False) + champion = Column(String(50), nullable=True) + orange_cap = Column(Text, nullable=True) + purple_cap = Column(Text, nullable=True) + best_strike_rate = Column(Text, nullable=True) + best_average = Column(Text, nullable=True) + best_economy = Column(Text, nullable=True) + player_of_tournament = Column(Text, nullable=True) + +# ── Check if tournament_history table exists ────────────────────────────────────── + +inspector = inspect(engine) +if "tournament_history" not in inspector.get_table_names(): + print("ERROR: tournament_history table not found in database.") + sys.exit(1) + +db = Session() + +# ── Find and fix problematic tournaments ────────────────────────────────────────── + +PROBLEMATIC_IDS = {'c6e5', '64c5', '8247', '369c', '1738', '088b'} + +print("--- Fixing tournament timestamps ---") +print() + +# 1. Fix specific tournament IDs +print("Step 1: Fixing specific problematic tournament IDs...") +fixed_count = 0 +for tid in PROBLEMATIC_IDS: + tournament = db.query(TournamentHistory).filter( + TournamentHistory.tournament_id == tid + ).first() + if tournament: + old_ts = tournament.timestamp + tournament.timestamp = datetime.utcnow() + db.commit() + print(f" ✓ {tid}: Updated from {old_ts} to {tournament.timestamp}") + fixed_count += 1 + else: + print(f" - {tid}: Not found in database") + +print(f" Fixed {fixed_count} specific tournament IDs") +print() + +# 2. Fix tournaments with NULL or epoch (01/01/1970) timestamps +print("Step 2: Fixing tournaments with NULL or epoch timestamps...") + +# Find NULL timestamps +null_count = 0 +null_tournaments = db.query(TournamentHistory).filter( + TournamentHistory.timestamp == None +).all() + +for tournament in null_tournaments: + tournament.timestamp = datetime.utcnow() + db.commit() + print(f" ✓ {tournament.tournament_id}: Updated from NULL to {tournament.timestamp}") + null_count += 1 + +print(f" Fixed {null_count} tournaments with NULL timestamps") +print() + +# Find epoch (01/01/1970) timestamps +epoch_count = 0 +all_tournaments = db.query(TournamentHistory).all() + +for tournament in all_tournaments: + if tournament.timestamp and tournament.timestamp.year == 1970 and \ + tournament.timestamp.month == 1 and tournament.timestamp.day == 1: + old_ts = tournament.timestamp + tournament.timestamp = datetime.utcnow() + db.commit() + print(f" ✓ {tournament.tournament_id}: Updated from {old_ts} to {tournament.timestamp}") + epoch_count += 1 + +print(f" Fixed {epoch_count} tournaments with epoch (01/01/1970) timestamps") +print() + +# ── Summary ──────────────────────────────────────────────────────────────────────── + +total_fixed = fixed_count + null_count + epoch_count +print("=" * 50) +print(f"Total tournaments fixed: {total_fixed}") +print("=" * 50) + +db.close() + +if total_fixed > 0: + print("✅ Timestamp fixes applied successfully!") +else: + print("ℹ No problematic timestamps found.") diff --git a/CricketGame/restore_data.py b/CricketGame/restore_data.py index a3b2552..e09136a 100644 --- a/CricketGame/restore_data.py +++ b/CricketGame/restore_data.py @@ -188,6 +188,17 @@ def _dt(s): return None +def _dt_with_fallback(s): + """Parse ISO timestamp string → datetime, or current UTC time as fallback. + This prevents timestamps from defaulting to epoch (01/01/1970).""" + if not s: + return datetime.utcnow() + try: + return datetime.fromisoformat(s.replace("Z", "+00:00").split("+")[0]) + except Exception: + return datetime.utcnow() + + def _j(obj): return json.dumps(obj) if obj is not None else None @@ -290,8 +301,8 @@ def _j(obj): match_id = mid, room_code = m.get("room_code", ""), mode = m.get("mode", "1v1"), - timestamp = _dt(m.get("timestamp")), - end_timestamp = _dt(m.get("end_timestamp")), + timestamp = _dt_with_fallback(m.get("timestamp")), + end_timestamp = _dt_with_fallback(m.get("end_timestamp")), side_a = _j(m.get("side_a", [])), side_b = _j(m.get("side_b", [])), scorecard_1 = _j(m.get("scorecard_1", {})), @@ -324,7 +335,7 @@ def _j(obj): row = TournamentHistory( tournament_id = tid, room_code = t.get("room_code", ""), - timestamp = _dt(t.get("timestamp")), + timestamp = _dt_with_fallback(t.get("timestamp")), players = _j(t.get("players", [])), standings = _j(t.get("standings", [])), playoff_bracket = _j(t.get("playoff_bracket")), diff --git a/TOURNAMENT_TIMESTAMP_FIX.md b/TOURNAMENT_TIMESTAMP_FIX.md new file mode 100644 index 0000000..fa42ec8 --- /dev/null +++ b/TOURNAMENT_TIMESTAMP_FIX.md @@ -0,0 +1,85 @@ +# Tournament Timestamp Fix - Implementation Summary + +## Problem +The following tournaments had invalid timestamps (01/01/1970 - Unix epoch): +- c6e5 +- 64c5 +- 8247 +- 369c +- 1738 +- 088b + +This was caused by: +1. `restore_data.py` returning `None` when timestamp parsing failed, which SQLAlchemy stored as epoch (01/01/1970) +2. `realtime/tournament.py` not explicitly setting the timestamp when saving tournament history +3. Relying on `server_default=func.now()` which doesn't always work reliably across all database configurations + +## Changes Made + +### 1. **backend/realtime/tournament.py** + - **Line 191**: Added explicit `timestamp=datetime.utcnow()` when creating `TournamentHistory` objects + - This ensures all new tournaments always have a valid timestamp, never relying on database defaults + +### 2. **restore_data.py** + - **Lines 191-200**: Added new function `_dt_with_fallback()` that returns current UTC time as fallback instead of None + - **Line 305**: Match history timestamps now use `_dt_with_fallback()` instead of `_dt()` + - **Line 338**: Tournament history timestamps now use `_dt_with_fallback()` instead of `_dt()` + - This prevents NULL timestamps from being stored, which could default to epoch + +### 3. **fix_tournament_timestamps.py** (NEW) + - Migration script to fix existing tournaments with problematic timestamps + - Targets specific tournament IDs (c6e5, 64c5, 8247, 369c, 1738, 088b) + - Also fixes any other tournaments with NULL or epoch (01/01/1970) timestamps + +## How to Apply Fixes + +### For Future Tournaments +The code changes in `backend/realtime/tournament.py` and `restore_data.py` will automatically prevent this issue for: +- Any tournaments created going forward (due to explicit timestamp in `save_tournament_history`) +- Any tournaments imported via `restore_data.py` (due to `_dt_with_fallback` function) + +### For Existing Problematic Tournaments +Run the migration script to fix the specific tournament IDs: + +```bash +cd ~/www/CricketGame # or wherever your app is installed +python fix_tournament_timestamps.py +``` + +This script will: +1. Find and update the specific problematic tournament IDs with current datetime +2. Find and fix any other tournaments with NULL timestamps +3. Find and fix any other tournaments with epoch (01/01/1970) timestamps +4. Report the number of tournaments fixed + +## Database Verification + +After running the migration script, verify the fixes by checking the database: + +```bash +cd ~/www/CricketGame +python3 << 'EOF' +from sqlalchemy import create_engine, text +import os + +DATABASE_URL = os.environ.get("DATABASE_URL") or "sqlite:///cricket.db" +engine = create_engine(DATABASE_URL) + +with engine.connect() as conn: + # Check the specific tournament IDs + for tid in ['c6e5', '64c5', '8247', '369c', '1738', '088b']: + result = conn.execute(text( + f"SELECT tournament_id, timestamp FROM tournament_history WHERE tournament_id = '{tid}'" + )).fetchone() + if result: + print(f"{tid}: {result[1]}") + else: + print(f"{tid}: Not found") +EOF +``` + +## Notes +- All timestamps in the database are in UTC (using `datetime.utcnow()`) +- The migration script uses `datetime.utcnow()` to ensure consistency +- Running the migration script multiple times is safe - it will update timestamps again if needed +- No other tournament data is modified, only the timestamp field