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
+26 -14
View File
@@ -5,8 +5,8 @@
use bevy::prelude::*;
use chrono::Local;
use solitaire_data::{
current_iso_week_key, save_progress_to, weekly_goal_by_id, WeeklyGoalContext, WEEKLY_GOALS,
WEEKLY_GOAL_XP,
WEEKLY_GOAL_XP, WEEKLY_GOALS, WeeklyGoalContext, current_iso_week_key, save_progress_to,
weekly_goal_by_id,
};
use crate::events::{GameWonEvent, XpAwardedEvent};
@@ -51,9 +51,10 @@ fn roll_weekly_goals_on_startup(
let week_key = current_iso_week_key(Local::now().date_naive());
if progress.0.roll_weekly_goals_if_new_week(&week_key)
&& let Some(target) = &path.0
&& let Err(e) = save_progress_to(target, &progress.0) {
warn!("failed to save progress after weekly reset on startup: {e}");
}
&& let Err(e) = save_progress_to(target, &progress.0)
{
warn!("failed to save progress after weekly reset on startup: {e}");
}
}
fn evaluate_weekly_goals(
@@ -114,9 +115,10 @@ fn evaluate_weekly_goals(
if any_change
&& let Some(target) = &path.0
&& let Err(e) = save_progress_to(target, &progress.0) {
warn!("failed to save progress after weekly goal update: {e}");
}
&& let Err(e) = save_progress_to(target, &progress.0)
{
warn!("failed to save progress after weekly goal update: {e}");
}
}
/// Resolve a goal id to its description (used for toasts).
@@ -196,10 +198,14 @@ mod tests {
// keeping the XP delta predictable.
{
let mut p = app.world_mut().resource_mut::<ProgressResource>();
p.0.weekly_goal_progress.insert("weekly_3_fast".to_string(), 2);
p.0.weekly_goal_progress.insert("weekly_1_under_five".to_string(), 1);
p.0.weekly_goal_progress.insert("weekly_5_wins".to_string(), 5);
p.0.weekly_goal_progress.insert("weekly_3_no_undo".to_string(), 3);
p.0.weekly_goal_progress
.insert("weekly_3_fast".to_string(), 2);
p.0.weekly_goal_progress
.insert("weekly_1_under_five".to_string(), 1);
p.0.weekly_goal_progress
.insert("weekly_5_wins".to_string(), 5);
p.0.weekly_goal_progress
.insert("weekly_3_no_undo".to_string(), 3);
}
// Match the current ISO week key so roll_weekly_goals doesn't clear it.
let key = current_iso_week_key(Local::now().date_naive());
@@ -263,7 +269,10 @@ mod tests {
fn weekly_bonus_xp_fires_levelup_when_threshold_crossed() {
let mut app = headless_app();
// Set XP just below the first level boundary (500) so the 75-XP bonus crosses it.
app.world_mut().resource_mut::<ProgressResource>().0.total_xp = 430;
app.world_mut()
.resource_mut::<ProgressResource>()
.0
.total_xp = 430;
// Pre-set goal to 2/3 so the next fast win completes it.
app.world_mut()
.resource_mut::<ProgressResource>()
@@ -285,7 +294,10 @@ mod tests {
let events = app.world().resource::<Messages<LevelUpEvent>>();
let mut cursor = events.get_cursor();
let fired: Vec<_> = cursor.read(events).copied().collect();
assert!(!fired.is_empty(), "LevelUpEvent must fire when weekly bonus pushes past a level threshold");
assert!(
!fired.is_empty(),
"LevelUpEvent must fire when weekly bonus pushes past a level threshold"
);
}
#[test]