fix(engine,server): safe area clamp, analytics batch, achievement save order, daily rollover, replay validation, leaderboard opt-in (#56, #60, #61, #62, #66, #68)
Build and Deploy / build-and-push (push) Successful in 3m54s
Build and Deploy / build-and-push (push) Successful in 3m54s
- #66: Clamp safe-area insets to 25% of window height with warn!() on excess - #68: Move fire_flush outside per-event loop in analytics (batch flush once) - #56: Persist progress before marking reward_granted to prevent XP loss on crash - #60: Add DateRolloverTimer + check_date_rollover system for midnight seed refresh - #62: Add validate_header() in replay upload with mode/draw_mode allowlists - #61: Restore two-query leaderboard opt-in check (SELECT then UPDATE); original queries already in .sqlx cache; EXISTS variant would require sqlx prepare Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -3,13 +3,13 @@
|
||||
//! `GET /api/leaderboard` — list all opted-in entries (requires auth).
|
||||
//! `POST /api/leaderboard/opt-in` — opt in and set / update display name.
|
||||
|
||||
use axum::{extract::State, Json};
|
||||
use axum::{Json, extract::State};
|
||||
use chrono::Utc;
|
||||
use serde::Deserialize;
|
||||
|
||||
use solitaire_sync::LeaderboardEntry;
|
||||
|
||||
use crate::{error::AppError, middleware::AuthenticatedUser, AppState};
|
||||
use crate::{AppState, error::AppError, middleware::AuthenticatedUser};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Request shapes
|
||||
@@ -118,7 +118,9 @@ pub async fn opt_in(
|
||||
) -> Result<Json<serde_json::Value>, AppError> {
|
||||
let display_name = body.display_name.trim();
|
||||
if display_name.is_empty() {
|
||||
return Err(AppError::BadRequest("display_name must not be empty".into()));
|
||||
return Err(AppError::BadRequest(
|
||||
"display_name must not be empty".into(),
|
||||
));
|
||||
}
|
||||
if display_name.chars().count() > DISPLAY_NAME_MAX {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
@@ -197,9 +199,9 @@ mod tests {
|
||||
fn leaderboard_entries_sorted_by_score_descending() {
|
||||
let mut entries = [
|
||||
entry("Charlie", Some(1_200)),
|
||||
entry("Alice", Some(8_000)),
|
||||
entry("Bob", Some(3_500)),
|
||||
entry("Dave", None), // no score — should rank last
|
||||
entry("Alice", Some(8_000)),
|
||||
entry("Bob", Some(3_500)),
|
||||
entry("Dave", None), // no score — should rank last
|
||||
];
|
||||
|
||||
// Mirrors the SQL sort:
|
||||
@@ -214,10 +216,22 @@ mod tests {
|
||||
});
|
||||
|
||||
// Scored entries first, in descending order.
|
||||
assert_eq!(entries[0].display_name, "Alice", "highest scorer must be first");
|
||||
assert_eq!(entries[1].display_name, "Bob", "second-highest scorer must be second");
|
||||
assert_eq!(entries[2].display_name, "Charlie", "lowest scorer must be third");
|
||||
assert_eq!(
|
||||
entries[0].display_name, "Alice",
|
||||
"highest scorer must be first"
|
||||
);
|
||||
assert_eq!(
|
||||
entries[1].display_name, "Bob",
|
||||
"second-highest scorer must be second"
|
||||
);
|
||||
assert_eq!(
|
||||
entries[2].display_name, "Charlie",
|
||||
"lowest scorer must be third"
|
||||
);
|
||||
// Null-score entry sinks to the bottom.
|
||||
assert_eq!(entries[3].display_name, "Dave", "entry with no score must rank last");
|
||||
assert_eq!(
|
||||
entries[3].display_name, "Dave",
|
||||
"entry with no score must rank last"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user