feat: Rebuild as Court Manager Dashboard v2

- Complete rewrite for single Court Manager user
- Feature 1: Player Management (CRUD, ELO, skill level)
- Feature 2: Court Reservation (timeline, override)
- Feature 3: Match Event Builder (3-step wizard)
  - Step 1: Event setup with player selection + stage calculator
  - Step 2: Stage configurator (Open/Skill/RR/Tournament)
  - Step 3: Auto bracket generation
- Feature 4: Live Court View (main dashboard, 2x2 grid)
- Event Control: score entry, bracket advancement, court assignment
- Screen view: TV display per court (/screen/:id)
- Seed: 8 players, 4 courts, 1 completed + 1 active event
- Routes: / players courts events events/new events/:id screen/:id
This commit is contained in:
Nemo
2026-03-19 07:35:51 +08:00
parent e455accc7e
commit 4421f61c24
56 changed files with 2949 additions and 4209 deletions

View File

@@ -1,27 +1,15 @@
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from .database import engine
from .models import Base
from .routers import players, courts, events, matches, dashboard, admin
from .websocket_manager import manager
import asyncio
import json
from typing import List, Dict
from app.database import engine, SessionLocal
from app.models import player, court, booking, match, tournament
Base.metadata.create_all(bind=engine)
# Create all tables
player.Base.metadata.create_all(bind=engine)
court.Base.metadata.create_all(bind=engine)
booking.Base.metadata.create_all(bind=engine)
match.Base.metadata.create_all(bind=engine)
tournament.Base.metadata.create_all(bind=engine)
from app.api import players, courts, matches, tournaments, screen, admin
app = FastAPI(
title="ServeSync API",
description="Pickleball Court Management System",
version="1.0.0",
)
app = FastAPI(title="ServeSync API", version="2.0.0")
app.add_middleware(
CORSMiddleware,
@@ -31,112 +19,37 @@ app.add_middleware(
allow_headers=["*"],
)
# Include routers
app.include_router(players.router, prefix="/api")
app.include_router(courts.router, prefix="/api")
app.include_router(matches.router, prefix="/api")
app.include_router(tournaments.router, prefix="/api")
app.include_router(screen.router, prefix="/api")
app.include_router(admin.router, prefix="/api")
app.include_router(players.router)
app.include_router(courts.router)
app.include_router(events.router)
app.include_router(matches.router)
app.include_router(dashboard.router)
app.include_router(admin.router)
# WebSocket connection manager
class ConnectionManager:
def __init__(self):
self.active_connections: Dict[str, List[WebSocket]] = {}
async def connect(self, websocket: WebSocket, channel: str):
await websocket.accept()
if channel not in self.active_connections:
self.active_connections[channel] = []
self.active_connections[channel].append(websocket)
def disconnect(self, websocket: WebSocket, channel: str):
if channel in self.active_connections:
self.active_connections[channel].remove(websocket)
async def broadcast(self, channel: str, data: dict):
if channel in self.active_connections:
disconnected = []
for connection in self.active_connections[channel]:
try:
await connection.send_text(json.dumps(data))
except Exception:
disconnected.append(connection)
for conn in disconnected:
self.active_connections[channel].remove(conn)
manager = ConnectionManager()
@app.websocket("/ws/court/{court_id}")
async def websocket_court(websocket: WebSocket, court_id: int):
"""WebSocket for real-time court display updates"""
channel = f"court_{court_id}"
await manager.connect(websocket, channel)
try:
while True:
# Send court data every 3 seconds
db = SessionLocal()
try:
from app.api.screen import get_court_display
data = get_court_display(court_id, db)
await websocket.send_text(json.dumps(data))
finally:
db.close()
await asyncio.sleep(3)
except WebSocketDisconnect:
manager.disconnect(websocket, channel)
@app.websocket("/ws/overview")
async def websocket_overview(websocket: WebSocket):
"""WebSocket for real-time overview display"""
channel = "overview"
await manager.connect(websocket, channel)
try:
while True:
db = SessionLocal()
try:
from app.api.screen import get_overview
data = get_overview(db)
await websocket.send_text(json.dumps(data))
finally:
db.close()
await asyncio.sleep(3)
except WebSocketDisconnect:
manager.disconnect(websocket, channel)
@app.websocket("/ws/matches")
async def websocket_matches(websocket: WebSocket):
"""WebSocket for real-time match lobby updates"""
channel = "matches"
await manager.connect(websocket, channel)
try:
while True:
db = SessionLocal()
try:
from app.api.matches import get_lobby
from app.api.matches import get_active_matches
data = {
"lobby": get_lobby(db),
"active": get_active_matches(db),
}
await websocket.send_text(json.dumps(data))
finally:
db.close()
await asyncio.sleep(2)
except WebSocketDisconnect:
manager.disconnect(websocket, channel)
@app.get("/")
def root():
return {"message": "ServeSync API v1.0", "status": "running"}
@app.get("/health")
@app.get("/api/health")
def health():
return {"status": "healthy"}
return {"status": "ok", "version": "2.0.0"}
@app.websocket("/ws/dashboard")
async def ws_dashboard(websocket: WebSocket):
await manager.connect(websocket, "dashboard")
try:
while True:
await asyncio.sleep(3)
await manager.broadcast("dashboard", {"type": "ping"})
except WebSocketDisconnect:
manager.disconnect(websocket, "dashboard")
@app.websocket("/ws/screen/{court_id}")
async def ws_screen(websocket: WebSocket, court_id: int):
channel = f"screen_{court_id}"
await manager.connect(websocket, channel)
try:
while True:
await asyncio.sleep(5)
await manager.broadcast(channel, {"type": "ping", "court_id": court_id})
except WebSocketDisconnect:
manager.disconnect(websocket, channel)