# 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 `<style>`. Index pages get it; sibling pages don't. Always add it to every new page.
- **Registration returns `display_name: null`** — The `/api/auth/register` endpoint must pass `display_name=req.display_name` in the response, not just write it to the DB. Login endpoint does this correctly (reads from DB); register endpoint must be patched to match.
- **Cross-account DNS** — A records work where CNAMEs silently fail for Cloudflare cross-account setups.
- **Serve both frontend and API from one backend** — Full-proxy through FastAPI + FileResponse catch-all avoids Caddy routing conflicts.
