4.5 KiB
4.5 KiB
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()vsdatetime.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:
@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
localStorageasshark_token, user asshark_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/leaguesto 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:
- DRAFT — Map view (Leaflet/OSM) | List view toggle
- MY TEAM — User's drafted regions + cumulative score
- STANDINGS — Leaderboard (all players ranked) + daily breakdown
- 🦈 LEARN — Species guide, shark facts, safety tips, conservation
- LEAGUE — Invite code, member list, start draft button (commissioner)
- 🦈 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
.hiddenCSS class — New standalone pages often define.hiddenvia 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/registerendpoint must passdisplay_name=req.display_namein 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.