Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CricketGame/backend/realtime/match/match_persistence.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
2 changes: 2 additions & 0 deletions CricketGame/backend/realtime/tournament.py
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
157 changes: 157 additions & 0 deletions CricketGame/fix_tournament_timestamps.py
Original file line number Diff line number Diff line change
@@ -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)
Comment on lines +51 to +52

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()
Comment on lines +95 to +109

# 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()
Comment on lines +114 to +127

# 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()
Comment on lines +129 to +143

# ── 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.")
17 changes: 14 additions & 3 deletions CricketGame/restore_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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", {})),
Expand Down Expand Up @@ -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")),
Expand Down
85 changes: 85 additions & 0 deletions TOURNAMENT_TIMESTAMP_FIX.md
Original file line number Diff line number Diff line change
@@ -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