Phase 25: leaderboard table + trade history in dashboard

- Division tab: leaderboard table (10 clubs, color-coded promo/relegation rows,
  player row highlighted, sorted by pts)
- Market tab: trade history table (last 30 buy/sell events, card name/OVR/price/date)
  loaded alongside market listings and my listings
- Mapper: added GET /division/leaderboard and GET /trade/history exact routes
- Mapper assertion bumped to ≥61

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-06-25 19:13:33 -07:00
parent 7c779e0d72
commit 77af7ce3c9
2 changed files with 93 additions and 3 deletions
+80 -1
View File
@@ -324,6 +324,21 @@ h2 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em;
.cd-stat-fill.med { background:#d29922; } .cd-stat-fill.med { background:#d29922; }
.cd-stat-fill.low { background:#f85149; } .cd-stat-fill.low { background:#f85149; }
.cd-actions { display:flex; gap:8px; margin-top:16px; flex-wrap:wrap; } .cd-actions { display:flex; gap:8px; margin-top:16px; flex-wrap:wrap; }
/* ── Leaderboard ── */
.ldb-table { width:100%; border-collapse:collapse; font-size:0.82rem; }
.ldb-table th { text-align:left; color:#8b949e; font-weight:600; border-bottom:1px solid #30363d; padding:6px 8px; }
.ldb-table td { padding:6px 8px; border-bottom:1px solid #161b22; }
.ldb-table tr.ldb-player { background:#1f2d1f; }
.ldb-table tr.ldb-promote td:first-child { border-left:3px solid #3fb950; }
.ldb-table tr.ldb-relegate td:first-child { border-left:3px solid #f85149; }
/* ── Trade History ── */
.trade-table { width:100%; border-collapse:collapse; font-size:0.82rem; }
.trade-table th { text-align:left; color:#8b949e; font-weight:600; border-bottom:1px solid #30363d; padding:6px 8px; }
.trade-table td { padding:6px 8px; border-bottom:1px solid #161b22; }
.trade-buy { color:#3fb950; font-weight:700; }
.trade-sell { color:#f5a623; font-weight:700; }
</style> </style>
</head> </head>
<body> <body>
@@ -529,6 +544,10 @@ h2 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em;
</div> </div>
</div> </div>
</div> </div>
<div class="section" style="margin-top:28px">
<h2>Division Leaderboard</h2>
<div id="division-leaderboard"><span class="loading">Loading…</span></div>
</div>
</div> </div>
<!-- ── FUT Champions tab ─────────────────────────────────────────────────── --> <!-- ── FUT Champions tab ─────────────────────────────────────────────────── -->
@@ -820,6 +839,13 @@ h2 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em;
<div id="my-listings"><span class="loading">Loading…</span></div> <div id="my-listings"><span class="loading">Loading…</span></div>
</div> </div>
</div> </div>
<div class="section" style="margin-top:28px">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
<h2 style="border:none;margin:0">Trade History</h2>
<button class="btn btn-secondary btn-sm" onclick="loadTradeHistory()">Refresh</button>
</div>
<div id="trade-history"><span class="loading">Loading…</span></div>
</div>
</div> </div>
<!-- ── Events tab ────────────────────────────────────────────────────────── --> <!-- ── Events tab ────────────────────────────────────────────────────────── -->
@@ -1437,6 +1463,34 @@ async function loadDivision() {
}).join(''); }).join('');
} }
} catch (_) { el('season-history').innerHTML = '<span class="empty">History unavailable.</span>'; } } catch (_) { el('season-history').innerHTML = '<span class="empty">History unavailable.</span>'; }
// Leaderboard
try {
const lb = await api('GET', '/division/leaderboard');
const rows = lb.leaderboard || [];
const promoLine = lb.promotion_threshold ?? 3;
const relLine = lb.relegation_threshold ?? 8;
el('division-leaderboard').innerHTML = `
<div style="font-size:0.72rem;color:#8b949e;margin-bottom:8px">
Division ${lb.division ?? '?'} · Season ${lb.season_number ?? '?'}
&nbsp;|&nbsp;<span style="color:#3fb950">●</span> Top ${promoLine} promote
&nbsp;|&nbsp;<span style="color:#f85149">●</span> Bottom ${10 - relLine} relegate
</div>
<table class="ldb-table">
<thead><tr><th>#</th><th>Club</th><th>W</th><th>D</th><th>L</th><th>Pts</th></tr></thead>
<tbody>${rows.map(r => {
const pClass = r.position <= promoLine ? 'ldb-promote'
: r.position > relLine ? 'ldb-relegate' : '';
const playerCls = r.is_player ? 'ldb-player' : '';
return `<tr class="${pClass} ${playerCls}">
<td>${r.position}</td>
<td>${r.club_name}${r.is_player ? ' <span style="font-size:0.7rem;color:#f5a623">(you)</span>' : ''}</td>
<td>${r.wins}</td><td>${r.draws}</td><td>${r.losses}</td>
<td><strong>${r.pts}</strong></td>
</tr>`;
}).join('')}</tbody>
</table>`;
} catch (_) { el('division-leaderboard').innerHTML = '<span class="empty">Leaderboard unavailable.</span>'; }
} }
async function claimRivalsReward() { async function claimRivalsReward() {
@@ -2458,8 +2512,33 @@ function renderDraftSession(data) {
let allMarketListings = []; let allMarketListings = [];
async function loadTradeHistory() {
const box = el('trade-history');
if (!box) return;
try {
const d = await api('GET', '/market/trade-history');
const trades = d.trades || [];
if (!trades.length) {
box.innerHTML = '<span class="empty">No trade history yet.</span>';
return;
}
box.innerHTML = `<table class="trade-table">
<thead><tr><th>Card</th><th>OVR</th><th>Type</th><th>Price</th><th>Date</th></tr></thead>
<tbody>${trades.map(t => `<tr>
<td>${t.card_name}</td>
<td>${t.card_overall}</td>
<td class="trade-${t.trade_type}">${t.trade_type === 'buy' ? 'Bought' : 'Sold'}</td>
<td>${(t.price ?? 0).toLocaleString()}</td>
<td style="color:#8b949e">${new Date(t.traded_at).toLocaleDateString()}</td>
</tr>`).join('')}
</tbody></table>`;
} catch (e) {
box.innerHTML = `<span class="empty">Trade history unavailable.</span>`;
}
}
async function loadMarket() { async function loadMarket() {
await Promise.all([loadMarketListings(), loadMyListings()]); await Promise.all([loadMarketListings(), loadMyListings(), loadTradeHistory()]);
// Populate sell card dropdown from collection // Populate sell card dropdown from collection
if (!allCards.length) { if (!allCards.length) {
try { const d = await api('GET', '/collection'); allCards = d.collection || []; } catch (_) {} try { const d = await api('GET', '/collection'); allCards = d.collection || []; } catch (_) {}
+13 -2
View File
@@ -237,12 +237,23 @@ const EXACT: &[ExactRoute] = &[
notes: "FUT notifications (plural form) → Core notifications", notes: "FUT notifications (plural form) → Core notifications",
}, },
// ── Squad list ──────────────────────────────────────────────────────────── // ── Squad list ────────────────────────────────────────────────────────────
// ── Division / Season history ───────────────────────────────────────────── // ── Division / Season history / Leaderboard ───────────────────────────────
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/division/history", ea_method: "GET", ea_path: "/ut/game/fut/division/history",
core_method: "GET", core_path: "/division/history", core_method: "GET", core_path: "/division/history",
notes: "FUT division history → Core season history", notes: "FUT division history → Core season history",
}, },
ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/division/leaderboard",
core_method: "GET", core_path: "/division/leaderboard",
notes: "FUT division leaderboard → Core seeded NPC leaderboard",
},
// ── Market trade history ──────────────────────────────────────────────────
ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/trade/history",
core_method: "GET", core_path: "/market/trade-history",
notes: "FUT trade history → Core market trade history",
},
// ── Daily check-in ──────────────────────────────────────────────────────── // ── Daily check-in ────────────────────────────────────────────────────────
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/dailyObjective", ea_method: "GET", ea_path: "/ut/game/fut/dailyObjective",
@@ -721,7 +732,7 @@ mod tests {
#[test] #[test]
fn test_total_exact_routes_count() { fn test_total_exact_routes_count() {
assert!(EXACT.len() >= 59, "expected at least 59 exact mappings, got {}", EXACT.len()); assert!(EXACT.len() >= 61, "expected at least 61 exact mappings, got {}", EXACT.len());
} }
// ── Phase 22 new mappings ───────────────────────────────────────────────── // ── Phase 22 new mappings ─────────────────────────────────────────────────