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

- #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:
funman300
2026-05-28 13:07:22 -07:00
parent 8cb4c9808e
commit 6e407a3ea7
104 changed files with 6356 additions and 3092 deletions
+42 -8
View File
@@ -15,14 +15,49 @@
//! be served without scanning every blob.
use axum::{
extract::{Path, Query, State},
Json,
extract::{Path, Query, State},
};
use chrono::Utc;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{error::AppError, middleware::AuthenticatedUser, AppState};
use crate::{AppState, error::AppError, middleware::AuthenticatedUser};
// ---------------------------------------------------------------------------
// Validation
// ---------------------------------------------------------------------------
const KNOWN_MODES: &[&str] = &["Classic", "Zen", "TimeAttack", "Challenge", "Difficulty"];
const KNOWN_DRAW_MODES: &[&str] = &["DrawOne", "DrawThree"];
fn validate_header(h: &ReplayHeader) -> Result<(), AppError> {
if !KNOWN_DRAW_MODES.contains(&h.draw_mode.as_str()) {
return Err(AppError::BadRequest(format!(
"invalid draw_mode '{}'; expected one of {:?}",
h.draw_mode, KNOWN_DRAW_MODES
)));
}
if !KNOWN_MODES.contains(&h.mode.as_str()) {
return Err(AppError::BadRequest(format!(
"invalid mode '{}'; expected one of {:?}",
h.mode, KNOWN_MODES
)));
}
if h.time_seconds <= 0 || h.time_seconds > 86_400 {
return Err(AppError::BadRequest(format!(
"time_seconds {} out of range (186400)",
h.time_seconds
)));
}
if h.final_score < 0 || h.final_score > 1_000_000 {
return Err(AppError::BadRequest(format!(
"final_score {} out of range (01000000)",
h.final_score
)));
}
Ok(())
}
// ---------------------------------------------------------------------------
// Wire types
@@ -91,6 +126,8 @@ pub async fn upload(
let header: ReplayHeader = serde_json::from_value(payload.clone())
.map_err(|e| AppError::BadRequest(format!("replay JSON missing fields: {e}")))?;
validate_header(&header)?;
let id = Uuid::new_v4().to_string();
let received_at = Utc::now().to_rfc3339();
let replay_json = serde_json::to_string(&payload)?;
@@ -205,12 +242,9 @@ pub async fn get_by_id(
State(state): State<AppState>,
Path(id): Path<String>,
) -> Result<Json<serde_json::Value>, AppError> {
let row = sqlx::query!(
"SELECT replay_json FROM replays WHERE id = ?",
id,
)
.fetch_optional(&state.pool)
.await?;
let row = sqlx::query!("SELECT replay_json FROM replays WHERE id = ?", id,)
.fetch_optional(&state.pool)
.await?;
let replay_json = row
.ok_or_else(|| AppError::NotFound("replay not found".into()))?