# Shark Attack Fantasy Game — Full-Stack Pattern ## Architecture ``` iOS Safari → Cloudflare → Caddy (shark.iamgmb.com) → FastAPI (port 8083) ├── /api/* (auth, leagues, draft, scores) └── /* (static frontend via FileResponse catch-all) ``` ## Domain / DNS setup - Domain: `shark.iamgmb.com` (Cloudflare zone f1fb2d357b8ff0fab54c5856130ec9ed) - Points to Core's IP (152.53.192.33) via A record (not CNAME — CNAME flattens fail for cross-account zones) - DNS-only initially, switch to proxied after TLS cert provisions - Caddy full-proxy pattern: EVERYTHING routes through the backend on 8083 ## Backend features learned - **FastAPI monolithic server.py** — ~980 lines, no routers, all endpoints in one file - **SQLite** with WAL mode + foreign keys - **Auth:** bcrypt password hashing, PyJWT tokens, 30-day expiry - **JWT gotcha:** `datetime.utcnow()` vs `datetime.now(timezone.utc)` — PyJWT expects naive UTC - **Registration response must include `display_name`** — frontend uses it immediately - **Snake draft logic:** round 1: 1→N, round 2: N→1, round 3: 1→N. Track via round number: odd rounds are forward, even are reverse - **Scoring engine:** events table + materialized user_scores table recalculated on each insert - **Point values:** sighting=2, bite=5, fatality=10 ## Draft regulation - **Minimum 2 members** to start a draft (server enforces) - **Commissioner-only** start draft endpoint - **Turn enforcement by draft_order** — server rejects out-of-turn picks with 403 - **Region locking** — duplicate picks return 409 ## 32 Regions with ISAF-based data Top 5 regions by 10-year sighting count: | Region | Sightings | Bites | Fatalities | |--------|-----------|-------|-----------| | Australia East | 87 | 28 | 6 | | Florida East Coast | 85 | 25 | 1 | | Hawaii | 47 | 12 | 2 | | Australia West | 42 | 15 | 8 | | Caribbean | 40 | 10 | 2 | Full data saved to `/root/shark-game/references/shark-incident-data-10yr.md` sourced from ISAF year-by-year reports. ## Caddy full-proxy pattern When the backend serves BOTH API and static files, use: ``` shark.iamgmb.com { reverse_proxy 127.0.0.1:8083 } ``` Backend catch-all route: ```python @app.api_route("/{path_name:path}", methods=["GET"]) async def serve_frontend(path_name: str): if path_name.startswith("api/"): return {"error": "not found"} file_path = os.path.join(STATIC_DIR, path_name) if os.path.isfile(file_path): return FileResponse(file_path) return FileResponse(os.path.join(STATIC_DIR, "index.html")) ``` ## Frontend-JWT integration pattern - Token stored in `localStorage` as `shark_token`, user as `shark_user` - Auth guard on each page: `if (!getToken()) window.location.href = 'index.html';` - API base is empty string (same-origin) - Registration sends: `{email, password, display_name, phone?}` - After auth, checks `/api/leagues` to determine next screen - Pages: index.html (landing/register/login/league-mgmt), draft-room.html (draft/leaderboard/shark-feed/learn), league.html (settings/invite) ## Multi-tab draft room pattern The draft room has 6 tabs: 1. DRAFT — Map view (Leaflet/OSM) | List view toggle 2. MY TEAM — User's drafted regions + cumulative score 3. STANDINGS — Leaderboard (all players ranked) + daily breakdown 4. 🦈 LEARN — Species guide, shark facts, safety tips, conservation 5. LEAGUE — Invite code, member list, start draft button (commissioner) 6. 🦈 SHARK FEED — Upcoming shark media (Shark Week, movies) + general shark news ## AI scraper pattern - Daily cron: Firecrawl search → admin-ai classification → pending-scores.json - Region matching via keyword/fuzzy matching of article text/location - Dedup via processed-urls.json ## Common pitfalls - **Missing `.hidden` CSS class** — New standalone pages often define `.hidden` via JS but forget the CSS rule `.hidden { display: none !important; }` in `