feat: PWA auction assistant — mobile-first progressive web app

FastAPI backend:
- /api/players — search/filter 505 Serie A players with calibrated prices
- /api/player/name — full detail with ML predictions (quantile P10/P50/P90,
  starter probability, bandit bid recommendation, opponent bid estimate)
- /api/roster — CRUD for live auction roster with budget tracking
- /api/stats — league-wide price distribution
- All ML models loaded on startup (quantile, survival, bandit, opponent, budget)

PWA frontend:
- Installable on iOS/Android via manifest.json + service worker
- Dark theme matching Fantabeto design system
- Player search with role filter, quick-scan list
- Detail sheet with ML intelligence cards
- One-tap bid buttons (market, ML rec, max)
- Live budget bar, roster management
- Works offline for cached assets
- Mobile-first (480px max-width)

Launch: uvicorn pwa.api:app --port 8601
Live at: http://localhost:8601
This commit is contained in:
ramseshk
2026-08-12 12:38:00 +08:00
parent 2c818e292a
commit 35370c81f8
9 changed files with 1044 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<meta name="theme-color" content="#00D084">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link rel="manifest" href="/manifest.json">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚽</text></svg>">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css">
<title>Fantabeto — Auction</title>
</head>
<body>
<div class="app">
<!-- Header -->
<header>
<div style="display:flex;justify-content:space-between;align-items:center;">
<h1>⚽ <span>Fantabeto</span> Auction</h1>
<div id="ping" class="ping">● connected</div>
</div>
<div class="budget-bar">
<div class="label">SPENT</div>
<div class="value" id="budgetSpent">0</div>
<div class="progress">
<div class="progress-fill" id="budgetFill" style="width:0%;"></div>
</div>
<div class="label">LEFT</div>
<div class="value" id="budgetRemaining" style="color:var(--green);">500</div>
<div class="label">cr</div>
</div>
</header>
<!-- Tabs -->
<div class="tabs">
<button class="tab active" data-tab="auction" onclick="switchTab('auction')">🔍 Players</button>
<button class="tab" data-tab="roster" onclick="switchTab('roster')">📋 Roster <span id="rosterCount" style="font-size:10px;color:var(--muted);"></span></button>
<button class="tab" data-tab="stats" onclick="switchTab('stats')">📊 Stats</button>
</div>
<!-- Auction View -->
<div id="auctionView">
<div class="search-bar">
<input type="text" placeholder="Search players..." id="searchInput"
oninput="state.search=this.value;searchPlayers()">
<select onchange="state.roleFilter=this.value;searchPlayers()">
<option value="">All</option>
<option value="P">GK</option>
<option value="D">DEF</option>
<option value="C">MID</option>
<option value="A">FWD</option>
</select>
</div>
<div class="player-list" id="playerList">
<div class="empty">Loading players...</div>
</div>
</div>
<!-- Roster View -->
<div id="rosterView" style="display:none;">
<div style="padding:12px 16px;">
<button onclick="resetRoster()" style="background:var(--card);border:1px solid var(--red);color:var(--red);padding:8px 16px;border-radius:8px;cursor:pointer;font-size:12px;">
Reset Roster
</button>
<span style="font-size:11px;color:var(--muted);margin-left:8px;">
Quotas: P3 D8 C8 A6
</span>
</div>
<div class="player-list" id="rosterList">
<div class="empty">No players drafted yet.</div>
</div>
</div>
<!-- Stats View -->
<div id="statsView" style="display:none;">
<div class="empty">Loading stats...</div>
</div>
</div>
<!-- Detail overlay -->
<div class="detail-overlay" id="detailOverlay" style="display:none;" onclick="closeDetail()">
</div>
<script src="/app.js"></script>
</body>
</html>