diff --git a/src/dashboard.html b/src/dashboard.html
index 42298a3..ec95a05 100644
--- a/src/dashboard.html
+++ b/src/dashboard.html
@@ -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.low { background:#f85149; }
.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; }
@@ -529,6 +544,10 @@ h2 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em;
+
+
Division Leaderboard
+
Loading…
+
@@ -820,6 +839,13 @@ h2 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em;
Loading…
+
+
+
Trade History
+
+
+
Loading…
+
@@ -1437,6 +1463,34 @@ async function loadDivision() {
}).join('');
}
} catch (_) { el('season-history').innerHTML = 'History unavailable.'; }
+
+ // 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 = `
+
+ Division ${lb.division ?? '?'} · Season ${lb.season_number ?? '?'}
+ | ● Top ${promoLine} promote
+ | ● Bottom ${10 - relLine} relegate
+
+
+ | # | Club | W | D | L | Pts |
+ ${rows.map(r => {
+ const pClass = r.position <= promoLine ? 'ldb-promote'
+ : r.position > relLine ? 'ldb-relegate' : '';
+ const playerCls = r.is_player ? 'ldb-player' : '';
+ return `
+ | ${r.position} |
+ ${r.club_name}${r.is_player ? ' (you)' : ''} |
+ ${r.wins} | ${r.draws} | ${r.losses} |
+ ${r.pts} |
+
`;
+ }).join('')}
+
`;
+ } catch (_) { el('division-leaderboard').innerHTML = 'Leaderboard unavailable.'; }
}
async function claimRivalsReward() {
@@ -2458,8 +2512,33 @@ function renderDraftSession(data) {
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 = 'No trade history yet.';
+ return;
+ }
+ box.innerHTML = `
+ | Card | OVR | Type | Price | Date |
+ ${trades.map(t => `
+ | ${t.card_name} |
+ ${t.card_overall} |
+ ${t.trade_type === 'buy' ? 'Bought' : 'Sold'} |
+ ${(t.price ?? 0).toLocaleString()} |
+ ${new Date(t.traded_at).toLocaleDateString()} |
+
`).join('')}
+
`;
+ } catch (e) {
+ box.innerHTML = `Trade history unavailable.`;
+ }
+}
+
async function loadMarket() {
- await Promise.all([loadMarketListings(), loadMyListings()]);
+ await Promise.all([loadMarketListings(), loadMyListings(), loadTradeHistory()]);
// Populate sell card dropdown from collection
if (!allCards.length) {
try { const d = await api('GET', '/collection'); allCards = d.collection || []; } catch (_) {}
diff --git a/src/mapper.rs b/src/mapper.rs
index 9c7da51..d9cb0a1 100644
--- a/src/mapper.rs
+++ b/src/mapper.rs
@@ -237,12 +237,23 @@ const EXACT: &[ExactRoute] = &[
notes: "FUT notifications (plural form) → Core notifications",
},
// ── Squad list ────────────────────────────────────────────────────────────
- // ── Division / Season history ─────────────────────────────────────────────
+ // ── Division / Season history / Leaderboard ───────────────────────────────
ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/division/history",
core_method: "GET", core_path: "/division/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 ────────────────────────────────────────────────────────
ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/dailyObjective",
@@ -721,7 +732,7 @@ mod tests {
#[test]
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 ─────────────────────────────────────────────────