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
+24 -8
View File
@@ -6,12 +6,12 @@
//! generated on first request for that date, then stored in the database
//! so subsequent calls return the same value.
use axum::{extract::State, Json};
use axum::{Json, extract::State};
use chrono::Utc;
use solitaire_sync::ChallengeGoal;
use crate::{error::AppError, AppState};
use crate::{AppState, error::AppError};
// ---------------------------------------------------------------------------
// Seed generation
@@ -115,7 +115,9 @@ pub async fn daily_challenge(
.await?;
if let Some(r) = row {
let json = r.goal_json.ok_or_else(|| AppError::Internal("missing goal_json".into()))?;
let json = r
.goal_json
.ok_or_else(|| AppError::Internal("missing goal_json".into()))?;
let goal: ChallengeGoal = serde_json::from_str(&json)?;
return Ok(Json(goal));
}
@@ -148,7 +150,9 @@ pub async fn daily_challenge(
.fetch_one(&state.pool)
.await?;
let stored_json = stored.goal_json.ok_or_else(|| AppError::Internal("missing goal_json after insert".into()))?;
let stored_json = stored
.goal_json
.ok_or_else(|| AppError::Internal("missing goal_json after insert".into()))?;
let stored_goal: ChallengeGoal = serde_json::from_str(&stored_json)?;
Ok(Json(stored_goal))
}
@@ -165,13 +169,22 @@ mod tests {
#[test]
fn hash_date_differs_across_adjacent_days() {
assert_ne!(hash_date_to_u64("2026-04-26"), hash_date_to_u64("2026-04-27"));
assert_ne!(hash_date_to_u64("2026-04-26"), hash_date_to_u64("2026-04-25"));
assert_ne!(
hash_date_to_u64("2026-04-26"),
hash_date_to_u64("2026-04-27")
);
assert_ne!(
hash_date_to_u64("2026-04-26"),
hash_date_to_u64("2026-04-25")
);
}
#[test]
fn hash_date_differs_across_years() {
assert_ne!(hash_date_to_u64("2026-01-01"), hash_date_to_u64("2027-01-01"));
assert_ne!(
hash_date_to_u64("2026-01-01"),
hash_date_to_u64("2027-01-01")
);
}
#[test]
@@ -217,7 +230,10 @@ mod tests {
fn generate_goal_all_variants_have_sane_ranges() {
for variant_idx in 0u64..6 {
let g = generate_goal("2026-04-26", variant_idx);
assert!(!g.description.is_empty(), "variant {variant_idx}: description must not be empty");
assert!(
!g.description.is_empty(),
"variant {variant_idx}: description must not be empty"
);
if let Some(t) = g.max_time_secs {
assert!(
(60..=3600).contains(&t),