import os from pathlib import Path import json from datetime import datetime import httpx from fastapi import FastAPI, Request, Form from fastapi.responses import HTMLResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates import uvicorn app = FastAPI(title="SidelineWorks") BASE_DIR = Path(__file__).parent templates = Jinja2Templates(directory=str(BASE_DIR / "templates")) app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static") # ── Blog content (kept simple, no DB) ────────────────────────────────── BLOG_POSTS = [ { "slug": "sideline-technology-2026", "title": "Why Sideline Technology Is the Smartest Investment Athletic Departments Make", "date": "2026-04-28", "author": "SidelineWorks Team", "image": None, "summary": "From paper binders to instant digital records: how sideline technology transforms athletic training, cuts liability, and gives coaches the data they need.", "content": """
Every high school athletic trainer knows the drill: grab a clipboard, scribble notes on a sideline during the game, then spend Sunday morning typing those notes into a spreadsheet. It's been the workflow for decades — but it doesn't have to be.
Modern sideline technology is changing how athletic trainers work, and the schools adopting it are seeing real results: faster return-to-play decisions, better communication with coaches, and dramatically reduced liability exposure.
Paper-based sideline tracking isn't just inefficient — it's risky. When an athlete takes a hit on Friday night and there's no digital record of the assessment, your athletic department carries that liability. Miss a follow-up? That's a gap in the care continuum.
Schools that move to digital sideline tracking eliminate these gaps. Every assessment is timestamped, every communication is logged, and every return-to-play decision has a clear audit trail.
At $1,500-2,000 per school per year, sideline software is one of the lowest-cost, highest-impact investments an athletic department can make. Compare that to the cost of a single concussion lawsuit or the administrative hours spent managing paper forms.
District-wide adoption brings the per-school cost down even further while ensuring consistent care standards across all programs.
The 2026-2027 school year will see the first wave of AI-assisted sideline assessments — not replacing the AT's judgment, but supporting it with pattern recognition and comparative data. Schools already using digital sideline tools will be positioned to adopt these capabilities as they mature.
Ready to see it in action? Schedule a demo.
""".strip() }, { "slug": "return-to-play-framework", "title": "Building a Better Return-to-Play Framework", "date": "2026-04-14", "author": "SidelineWorks Team", "image": None, "summary": "How structured return-to-play protocols improve athlete outcomes and reduce re-injury rates across high school sports programs.", "content": """The numbers are stark: athletes who return to sport too quickly after injury face a 2-3x higher risk of re-injury. Yet many high school programs lack a structured, trackable return-to-play (RTP) framework.
A good RTP framework isn't just about safety — it's about performance. Athletes who complete all stages of a structured RTP protocol return stronger, more confident, and less likely to re-injure.
While every injury is unique, most RTP protocols follow a common progression:
Paper-based RTP tracking has three critical gaps:
SidelineWorks solves all three: automated milestone tracking, real-time coach notifications, and a complete audit trail for every RTP case.
A football offensive lineman's RTP looks very different from a soccer midfielder's. Good RTP software accounts for sport-specific demands and adjusts recovery milestones accordingly. SidelineWorks lets ATs customize RTP protocols per sport, per injury type, and per athlete.
""".strip() }, { "slug": "athletic-trainer-workflow", "title": "Why Athletic Trainers Are Drowning in Paperwork (And What to Do About It)", "date": "2026-03-30", "author": "SidelineWorks Team", "image": None, "summary": "The average high school AT spends 8+ hours per week on documentation. Sideline software cuts that to under 2 hours — here's how.", "content": """A 2024 survey of high school athletic trainers revealed a sobering stat: the average AT spends 8.3 hours per week on documentation alone. That's a full day of clinical work lost to forms, spreadsheets, and emails.
For ATs covering multiple schools or managing large programs, that number climbs even higher. The result? Less time with athletes, higher burnout rates, and documentation that's often incomplete.
Here's what a typical documentation workflow looks like without sideline software:
Each step adds friction. Each step is a place where information gets lost or delayed.
With SidelineWorks, that workflow becomes four taps on a phone:
ATs using SidelineWorks report documentation time dropping to under 2 hours per week. That's 6+ hours returned to clinical care.
The hidden benefit of digital-first documentation is the data it generates. With SidelineWorks, ATs can:
Paper forms can't do any of that.
Every hour an AT spends on paperwork is an hour not spent with an athlete. Sideline software is the simplest, highest-impact change a program can make to improve care quality and reduce AT burnout.
""".strip() }, ] # ── Routes ───────────────────────────────────────────────────────────── def tmpl(name, request, **kw): """Helper to render a template with Starlette 1.0+ signature.""" return templates.TemplateResponse(request, name, kw or None) @app.get("/", response_class=HTMLResponse) async def home(request: Request): return tmpl("home.html", request) @app.get("/product", response_class=HTMLResponse) async def product(request: Request): return tmpl("product.html", request) @app.get("/pricing", response_class=HTMLResponse) async def pricing(request: Request): return tmpl("pricing.html", request) @app.get("/demo", response_class=HTMLResponse) async def demo(request: Request): return tmpl("demo.html", request, submitted=False) @app.post("/demo") async def demo_submit( request: Request, name: str = Form(""), email: str = Form(""), school: str = Form(""), role: str = Form(""), message: str = Form(""), ): _notify_telegram( title="📋 New Demo Request", lines=[ f"Name: {name}", f"Email: {email}", f"School: {school}", f"Role: {role}", f"Message: {message}", ], ) return tmpl("demo.html", request, submitted=True) @app.get("/contact", response_class=HTMLResponse) async def contact(request: Request): return tmpl("contact.html", request, submitted=False) @app.post("/contact") async def contact_submit( request: Request, name: str = Form(""), email: str = Form(""), subject: str = Form(""), message: str = Form(""), ): _notify_telegram( title="📬 New Contact Message", lines=[ f"From: {name}", f"Email: {email}", f"Subject: {subject}", f"Message: {message}", ], ) return tmpl("contact.html", request, submitted=True) @app.get("/blog", response_class=HTMLResponse) async def blog_index(request: Request): return tmpl("blog.html", request, posts=BLOG_POSTS) @app.get("/blog/{slug}", response_class=HTMLResponse) async def blog_article(request: Request, slug: str): post = next((p for p in BLOG_POSTS if p["slug"] == slug), None) if not post: return templates.TemplateResponse(request, "404.html", status_code=404) return tmpl("article.html", request, post=post) # ── Health check ────────────────────────────────────────────────────── @app.get("/health") async def health(): return {"status": "ok", "service": "sidelineworks"} # ── Telegram notification helper ───────────────────────────────────── def _notify_telegram(title: str, lines: list[str]): """Send a formatted message to Matt via the primary Wadsworth bot.""" token = os.environ.get("TELEGRAM_BOT_TOKEN", "") chat_id = os.environ.get("TELEGRAM_MATT_CHAT_ID", "8386527252") if not token: print("[telegram] no TELEGRAM_BOT_TOKEN set, skipping notification") return text = f"{title}\n" + "\n".join(lines) url = f"https://api.telegram.org/bot{token}/sendMessage" import threading def _send(): try: with httpx.Client() as client: resp = client.post(url, json={ "chat_id": chat_id, "text": text, "parse_mode": "HTML", "disable_web_page_preview": True, }, timeout=10) if resp.status_code != 200: print(f"[telegram] send failed: {resp.status_code} {resp.text[:200]}") except Exception as e: print(f"[telegram] error: {e}") threading.Thread(target=_send, daemon=True).start() if __name__ == "__main__": uvicorn.run("main:app", host="0.0.0.0", port=8003, reload=True)