πŸ“„ main.py 9,363 bytes Today 04:45 πŸ“‹ Raw

from pathlib import Path
from typing import Any
import httpx
import logging

from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
import uvicorn

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("rtsport-dashboard")

app = FastAPI(title="RTSport Dashboard")

BASE_DIR = Path(file).parent
FRONTEND_DIR = BASE_DIR / "templates"
STATIC_DIR = BASE_DIR / "static"
API_BASE = "http://localhost:8001"

templates = Jinja2Templates(directory=str(FRONTEND_DIR))
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")

── API Proxy (transforms to match frontend expectations) ────────────

async def api_get(path: str, token: str) -> dict[str, Any]:
"""Call the real API and return JSON."""
async with httpx.AsyncClient(timeout=10) as client:
r = await client.get(
f"{API_BASE}{path}",
headers={"Authorization": f"Bearer {token}"},
)
if r.status_code == 401:
raise HTTPException(401, "Unauthorized β€” please log in")
if r.status_code == 403:
raise HTTPException(403, "Access denied")
r.raise_for_status()
return r.json()

def transform_teams(data: dict) -> dict:
"""Transform teams object β†’ array for frontend compatibility.
Also normalizes athlete fields: current_status β†’ status, etc."""
data = dict(data)
if isinstance(data.get("teams"), dict):
teams_obj = data["teams"]
teams_arr = []
for team_name, athletes in teams_obj.items():
fixed = []
for a in (athletes or []):
a = dict(a)
# Normalize fields
a["status"] = a.get("current_status", "cleared")
a["injury"] = {
"body_part": a.get("case_title", "") or "",
} if a.get("case_title") else None
a["team_name"] = team_name
fixed.append(a)
teams_arr.append({
"team_name": team_name,
"athletes": fixed,
})
data["teams"] = teams_arr
return data

def transform_at_stats(data: dict) -> dict:
"""Transform AT stats: full_count→cleared, out_count→out, modified_count→modified.
Also normalize active_cases fields."""
data = dict(data)
stats = data.get("stats", {})
if "full_count" in stats:
data["stats"] = {
"cleared": stats.get("full_count", 0),
"out": stats.get("out_count", 0),
"modified": stats.get("modified_count", 0),
}
# Normalize active_cases: athlete_id→id, athlete_name→name, injury_short→injury
if isinstance(data.get("active_cases"), list):
fixed = []
for c in data["active_cases"]:
c = dict(c)
c["id"] = c.pop("athlete_id", "")
c["name"] = c.pop("athlete_name", "")
c["injury"] = c.get("injury_short", c.get("injury", ""))
fixed.append(c)
data["active_cases"] = fixed
return data

def transform_ad_response(data: dict) -> dict:
"""Flatten AD response for frontend."""
data = dict(data)
data["school_stats"] = {
"total_athletes": data.get("total_athletes", 0),
"active_cases": data.get("active_cases", 0),
"out_today": data.get("out_today", 0),
}
# Transform sports fields: athletes→total_athletes, out→out_count, modified→modified_count
if isinstance(data.get("sports"), list):
fixed = []
for s in data["sports"]:
s = dict(s)
s["total_athletes"] = s.pop("athletes", 0)
s["out_count"] = s.pop("out", 0)
s["modified_count"] = s.pop("modified", 0)
fixed.append(s)
data["sports"] = fixed
# Transform activity items: time_ago→time
if isinstance(data.get("recent_activity"), list):
data["recent_activity"] = [
{"text": a.get("text", ""), "time": a.get("time_ago", "")}
for a in data["recent_activity"]
]
return data

── Auth helpers ─────────────────────────────────────────────────────

async def api_login(email: str, password: str) -> dict:
"""Authenticate and return token + user info."""
async with httpx.AsyncClient(timeout=10) as client:
r = await client.post(
f"{API_BASE}/api/v1/auth/login/json",
json={"email": email, "password": password},
)
if r.status_code != 200:
raise HTTPException(401, "Login failed")
return r.json()

── Routes ───────────────────────────────────────────────────────────

def tmpl(name, request, **kw):
return templates.TemplateResponse(request, name, kw or None)

@app.get("/ping")
async def ping():
return {"status": "ok"}

@app.get("/login", response_class=HTMLResponse)
async def login_page(request: Request):
return tmpl("login.html", request)

@app.post("/api/login")
async def login(request: Request):
body = await request.json()
email = body.get("email", "")
password = body.get("password", "")
result = await api_login(email, password)
return {
"token": result["access_token"],
"role": result["role"],
"school_id": result["school_id"],
"assigned_sports": result.get("assigned_sports", []),
}

── Dashboard pages ─────────────────────────────────────────────────

@app.get("/coach", response_class=HTMLResponse)
async def coach_dashboard(request: Request):
return tmpl("coach/dashboard.html", request)

@app.get("/at", response_class=HTMLResponse)
async def at_dashboard(request: Request):
return tmpl("at/dashboard.html", request)

@app.get("/parent", response_class=HTMLResponse)
async def parent_dashboard(request: Request):
return tmpl("parent/dashboard.html", request)

@app.get("/ad", response_class=HTMLResponse)
async def ad_dashboard(request: Request):
return tmpl("ad/dashboard.html", request)

@app.get("/at/sideline-entry", response_class=HTMLResponse)
async def sideline_entry(request: Request):
return tmpl("at/sideline-entry.html", request)

── API Proxy endpoints ──────────────────────────────────────────────

Coach dashboard — transform teams object→array

@app.get("/api/v1/dashboard/coach")
async def proxy_coach_dashboard(sport: str = "Football", school_id: str = "schl_001", request: Request = None):
auth = request.headers.get("Authorization", "")
data = await api_get(f"/api/v1/dashboard/coach?sport={sport}&school_id={school_id}", auth.replace("Bearer ", ""))
data = transform_teams(data)
return data

AT dashboard β€” fix stats field names

@app.get("/api/v1/dashboard/at")
async def proxy_at_dashboard(school_id: str = "schl_001", request: Request = None):
auth = request.headers.get("Authorization", "")
data = await api_get(f"/api/v1/dashboard/at?school_id={school_id}", auth.replace("Bearer ", ""))
data = transform_at_stats(data)
data = transform_teams(data)
return data

Parent dashboard β€” fix athlete_id param name

@app.get("/api/v1/dashboard/parent")
async def proxy_parent_dashboard(school_id: str = "schl_001", athlete_id: str = "", request: Request = None):
auth = request.headers.get("Authorization", "")
data = await api_get(f"/api/v1/dashboard/parent?school_id={school_id}&athlete_id={athlete_id}", auth.replace("Bearer ", ""))
return data

AD dashboard β€” flatten nested stats

@app.get("/api/v1/dashboard/ad")
async def proxy_ad_dashboard(school_id: str = "schl_001", request: Request = None):
auth = request.headers.get("Authorization", "")
data = await api_get(f"/api/v1/dashboard/ad?school_id={school_id}", auth.replace("Bearer ", ""))
data = transform_ad_response(data)
return data

Generic proxy for other API endpoints (roster, cases, etc.)

@app.get("/api/v1/{path:path}")
async def proxy_generic(path: str, request: Request):
auth = request.headers.get("Authorization", "")
query = str(request.url.query)
url = f"/api/v1/{path}"
if query:
url += f"?{query}"
data = await api_get(url, auth.replace("Bearer ", ""))
return data

── Startup / Seed Login Check ──────────────────────────────────────

@app.on_event("startup")
async def startup():
try:
async with httpx.AsyncClient(timeout=5) as client:
r = await client.get(f"{API_BASE}/health")
logger.info(f"RTSport API at {API_BASE}: {r.status_code}")
except Exception as e:
logger.warning(f"RTSport API unavailable ({e}). Dashboards will use mock data.")

if name == "main":
uvicorn.run("main:app", host="0.0.0.0", port=8002, reload=True)