🏓 Initial ServeSync demo build
- FastAPI backend with PostgreSQL + Redis - 4 core features: Registration, Courts, Matchmaking, Tournament - Double elimination tournament with bracket visualization - ELO-based matchmaking (Stage 1 Open, Stage 2 Skill-Based) - Real-time WebSocket updates - TV Screen display for courts - Vue 3 + Tailwind CSS frontend - Seed data: 12 players, 4 courts, active matches, tournament in progress - Docker compose stack with Nginx reverse proxy
This commit is contained in:
156
backend/app/api/screen.py
Normal file
156
backend/app/api/screen.py
Normal file
@@ -0,0 +1,156 @@
|
||||
"""Screen Display API - for TV/display views"""
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import desc
|
||||
from app.database import get_db
|
||||
from app.models.court import Court
|
||||
from app.models.match import Match, MatchPlayer, MatchStatus
|
||||
from app.models.player import Player
|
||||
from app.models.tournament import Tournament, TournamentStatus
|
||||
from app.services.tournament import get_bracket_data
|
||||
from datetime import datetime
|
||||
|
||||
router = APIRouter(prefix="/screen", tags=["screen"])
|
||||
|
||||
|
||||
@router.get("/court/{court_id}")
|
||||
def get_court_display(court_id: int, db: Session = Depends(get_db)):
|
||||
"""Full court display data for TV screen"""
|
||||
court = db.query(Court).filter(Court.id == court_id).first()
|
||||
if not court:
|
||||
return {"error": "Court not found"}
|
||||
|
||||
# Current active match
|
||||
active_match = db.query(Match).filter(
|
||||
Match.court_id == court_id,
|
||||
Match.status == MatchStatus.IN_PROGRESS
|
||||
).first()
|
||||
|
||||
match_data = None
|
||||
if active_match:
|
||||
team1_players = []
|
||||
team2_players = []
|
||||
for mp in active_match.match_players:
|
||||
p = db.query(Player).filter(Player.id == mp.player_id).first()
|
||||
if p:
|
||||
pd = {
|
||||
"id": p.id,
|
||||
"name": p.name,
|
||||
"elo": round(p.elo_rating, 1),
|
||||
"avatar_color": p.avatar_color,
|
||||
"membership_tier": p.membership_tier.value,
|
||||
}
|
||||
if mp.team == 1:
|
||||
team1_players.append(pd)
|
||||
else:
|
||||
team2_players.append(pd)
|
||||
|
||||
elapsed = None
|
||||
if active_match.started_at:
|
||||
elapsed = int((datetime.utcnow() - active_match.started_at).total_seconds())
|
||||
|
||||
match_data = {
|
||||
"id": active_match.id,
|
||||
"stage": active_match.stage.value,
|
||||
"match_type": active_match.match_type.value,
|
||||
"team1": team1_players,
|
||||
"team2": team2_players,
|
||||
"team1_score": active_match.team1_score,
|
||||
"team2_score": active_match.team2_score,
|
||||
"elapsed_seconds": elapsed,
|
||||
}
|
||||
|
||||
# Recent completed matches on this court
|
||||
recent_matches = db.query(Match).filter(
|
||||
Match.court_id == court_id,
|
||||
Match.status == MatchStatus.COMPLETED
|
||||
).order_by(desc(Match.ended_at)).limit(3).all()
|
||||
|
||||
recent = []
|
||||
for m in recent_matches:
|
||||
team1 = [db.query(Player).filter(Player.id == mp.player_id).first().name
|
||||
for mp in m.match_players if mp.team == 1]
|
||||
team2 = [db.query(Player).filter(Player.id == mp.player_id).first().name
|
||||
for mp in m.match_players if mp.team == 2]
|
||||
recent.append({
|
||||
"team1": team1,
|
||||
"team2": team2,
|
||||
"team1_score": m.team1_score,
|
||||
"team2_score": m.team2_score,
|
||||
"winner": "team1" if m.team1_score > m.team2_score else "team2",
|
||||
})
|
||||
|
||||
return {
|
||||
"court": {
|
||||
"id": court.id,
|
||||
"name": court.name,
|
||||
"court_number": court.court_number,
|
||||
"surface_type": court.surface_type,
|
||||
},
|
||||
"active_match": match_data,
|
||||
"recent_matches": recent,
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/overview")
|
||||
def get_overview(db: Session = Depends(get_db)):
|
||||
"""All courts overview for main display"""
|
||||
courts = db.query(Court).filter(Court.is_active == True).order_by(Court.court_number).all()
|
||||
result = []
|
||||
for court in courts:
|
||||
active_match = db.query(Match).filter(
|
||||
Match.court_id == court.id,
|
||||
Match.status == MatchStatus.IN_PROGRESS
|
||||
).first()
|
||||
|
||||
court_data = {
|
||||
"id": court.id,
|
||||
"name": court.name,
|
||||
"court_number": court.court_number,
|
||||
"status": "occupied" if active_match else "available",
|
||||
}
|
||||
|
||||
if active_match:
|
||||
team1 = [mp.player.name for mp in active_match.match_players if mp.team == 1]
|
||||
team2 = [mp.player.name for mp in active_match.match_players if mp.team == 2]
|
||||
court_data["match"] = {
|
||||
"team1_names": team1,
|
||||
"team2_names": team2,
|
||||
"team1_score": active_match.team1_score,
|
||||
"team2_score": active_match.team2_score,
|
||||
"stage": active_match.stage.value,
|
||||
}
|
||||
|
||||
result.append(court_data)
|
||||
|
||||
# Active tournament
|
||||
tournament_data = None
|
||||
active_tournament = db.query(Tournament).filter(
|
||||
Tournament.status == TournamentStatus.IN_PROGRESS
|
||||
).first()
|
||||
if active_tournament:
|
||||
tournament_data = get_bracket_data(db, active_tournament.id)
|
||||
|
||||
# Leaderboard top 5
|
||||
top_players = db.query(Player).filter(Player.is_active == True).order_by(
|
||||
Player.elo_rating.desc()
|
||||
).limit(5).all()
|
||||
|
||||
return {
|
||||
"courts": result,
|
||||
"tournament": tournament_data,
|
||||
"leaderboard": [
|
||||
{
|
||||
"rank": i + 1,
|
||||
"name": p.name,
|
||||
"elo": round(p.elo_rating, 1),
|
||||
"tier": p.membership_tier.value,
|
||||
"wins": p.wins,
|
||||
"losses": p.losses,
|
||||
"avatar_color": p.avatar_color,
|
||||
}
|
||||
for i, p in enumerate(top_players)
|
||||
],
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
}
|
||||
Reference in New Issue
Block a user