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)