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)