diff --git a/.sqlx/query-0e199cafab7e71b0c7f28ede85a622e38649d2fe5a73a5c715f2319f5450f729.json b/.sqlx/query-0e199cafab7e71b0c7f28ede85a622e38649d2fe5a73a5c715f2319f5450f729.json new file mode 100644 index 0000000..c6ede40 --- /dev/null +++ b/.sqlx/query-0e199cafab7e71b0c7f28ede85a622e38649d2fe5a73a5c715f2319f5450f729.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE leaderboard\n SET best_score = ?,\n best_time_secs = ?,\n recorded_at = ?\n WHERE user_id = ?\n AND (\n best_score IS NULL\n OR ? > best_score\n OR (? = best_score AND (best_time_secs IS NULL OR ? < best_time_secs))\n )", + "describe": { + "columns": [], + "parameters": { + "Right": 7 + }, + "nullable": [] + }, + "hash": "0e199cafab7e71b0c7f28ede85a622e38649d2fe5a73a5c715f2319f5450f729" +} diff --git a/.sqlx/query-57c93a6acd7eea44d00412e62f0d3fed7ffbe4cd759353d29f38a8eb37f69112.json b/.sqlx/query-2b814989a6632ca930ae1e895f97a7fc3389c91d1d2abf6900a21fb0d6e94ef3.json similarity index 86% rename from .sqlx/query-57c93a6acd7eea44d00412e62f0d3fed7ffbe4cd759353d29f38a8eb37f69112.json rename to .sqlx/query-2b814989a6632ca930ae1e895f97a7fc3389c91d1d2abf6900a21fb0d6e94ef3.json index 14c7cbb..f66639d 100644 --- a/.sqlx/query-57c93a6acd7eea44d00412e62f0d3fed7ffbe4cd759353d29f38a8eb37f69112.json +++ b/.sqlx/query-2b814989a6632ca930ae1e895f97a7fc3389c91d1d2abf6900a21fb0d6e94ef3.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT l.display_name, l.best_score, l.best_time_secs, l.recorded_at\n FROM leaderboard l\n JOIN users u ON u.id = l.user_id\n WHERE u.leaderboard_opt_in = 1\n ORDER BY\n CASE WHEN l.best_score IS NULL THEN 1 ELSE 0 END ASC,\n l.best_score DESC,\n CASE WHEN l.best_time_secs IS NULL THEN 1 ELSE 0 END ASC,\n l.best_time_secs ASC", + "query": "SELECT l.display_name, l.best_score, l.best_time_secs, l.recorded_at\n FROM leaderboard l\n JOIN users u ON u.id = l.user_id\n WHERE u.leaderboard_opt_in = 1\n ORDER BY\n CASE WHEN l.best_score IS NULL THEN 1 ELSE 0 END ASC,\n l.best_score DESC,\n CASE WHEN l.best_time_secs IS NULL THEN 1 ELSE 0 END ASC,\n l.best_time_secs ASC\n LIMIT 100", "describe": { "columns": [ { @@ -34,5 +34,5 @@ false ] }, - "hash": "57c93a6acd7eea44d00412e62f0d3fed7ffbe4cd759353d29f38a8eb37f69112" + "hash": "2b814989a6632ca930ae1e895f97a7fc3389c91d1d2abf6900a21fb0d6e94ef3" } diff --git a/solitaire_server/src/leaderboard.rs b/solitaire_server/src/leaderboard.rs index eaa0e8f..7b9022d 100644 --- a/solitaire_server/src/leaderboard.rs +++ b/solitaire_server/src/leaderboard.rs @@ -54,7 +54,8 @@ pub async fn get_leaderboard( CASE WHEN l.best_score IS NULL THEN 1 ELSE 0 END ASC, l.best_score DESC, CASE WHEN l.best_time_secs IS NULL THEN 1 ELSE 0 END ASC, - l.best_time_secs ASC"# + l.best_time_secs ASC + LIMIT 100"# ) .fetch_all(&state.pool) .await?; diff --git a/solitaire_server/src/replays.rs b/solitaire_server/src/replays.rs index 78bd3dd..f158bab 100644 --- a/solitaire_server/src/replays.rs +++ b/solitaire_server/src/replays.rs @@ -114,6 +114,32 @@ pub async fn upload( .execute(&state.pool) .await?; + // Update leaderboard best score/time for opted-in users when this replay + // beats their existing best. Only classic mode counts for the leaderboard. + if header.mode == "classic" { + sqlx::query!( + r#"UPDATE leaderboard + SET best_score = ?, + best_time_secs = ?, + recorded_at = ? + WHERE user_id = ? + AND ( + best_score IS NULL + OR ? > best_score + OR (? = best_score AND (best_time_secs IS NULL OR ? < best_time_secs)) + )"#, + header.final_score, + header.time_seconds, + header.recorded_at, + user.user_id, + header.final_score, + header.final_score, + header.time_seconds, + ) + .execute(&state.pool) + .await?; + } + Ok(Json(ReplayUploadResponse { id })) } diff --git a/solitaire_server/web/game.js b/solitaire_server/web/game.js index 9df6326..9775f47 100644 --- a/solitaire_server/web/game.js +++ b/solitaire_server/web/game.js @@ -320,6 +320,30 @@ function showWin(s) { const sec = elapsedSecs % 60; if (winTime) winTime.textContent = `${m}:${sec.toString().padStart(2, "0")}`; winOverlay.classList.remove("hidden"); + submitReplay(s); +} + +async function submitReplay(s) { + const token = localStorage.getItem('fs_token'); + if (!token) return; + const payload = { + schema_version: 1, + seed: Math.round(game.seed()), + draw_mode: drawThree ? "draw_three" : "draw_one", + mode: "classic", + time_seconds: elapsedSecs, + final_score: s.score, + move_count: s.move_count, + recorded_at: new Date().toISOString(), + moves: [], + }; + try { + await fetch('/api/replays', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, + body: JSON.stringify(payload), + }); + } catch (_) { /* best-effort — never block the win screen */ } } // ── Auto-complete ───────────────────────────────────────────────────────────── diff --git a/solitaire_server/web/leaderboard.html b/solitaire_server/web/leaderboard.html index cde609a..cc2dee8 100644 --- a/solitaire_server/web/leaderboard.html +++ b/solitaire_server/web/leaderboard.html @@ -100,6 +100,9 @@