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
+51 -30
View File
@@ -37,13 +37,13 @@ use std::sync::Arc;
use bevy::input::ButtonState;
use bevy::input::keyboard::KeyboardInput;
use bevy::prelude::*;
use bevy::tasks::{futures_lite::future, AsyncComputeTaskPool, Task};
use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future};
use solitaire_data::{
auth_tokens::{delete_tokens, store_tokens},
settings::SyncBackend,
save_settings_to,
sync_client::{LocalOnlyProvider, SolitaireServerClient},
SyncError,
auth_tokens::{delete_tokens, store_tokens},
save_settings_to,
settings::SyncBackend,
sync_client::{LocalOnlyProvider, SolitaireServerClient},
};
use crate::avatar_plugin::AvatarFetchEvent;
@@ -53,15 +53,16 @@ use crate::events::{
};
use crate::font_plugin::FontResource;
use crate::platform::SHOW_KEYBOARD_ACCELERATORS;
use crate::settings_plugin::{SettingsPanel, SettingsResource, SettingsScreen, SettingsStoragePath};
use crate::resources::TokioRuntimeResource;
use crate::settings_plugin::{
SettingsPanel, SettingsResource, SettingsScreen, SettingsStoragePath,
};
use crate::sync_plugin::SyncProviderResource;
use crate::ui_modal::{spawn_modal, ModalScrim};
use crate::ui_modal::{ModalScrim, spawn_modal};
use crate::ui_theme::{
ACCENT_PRIMARY, BG_ELEVATED, BG_ELEVATED_HI,
BORDER_SUBTLE, HighContrastBorder, RADIUS_SM, STATE_DANGER, TEXT_DISABLED,
TEXT_PRIMARY, TEXT_SECONDARY, TYPE_BODY, TYPE_BODY_LG, TYPE_CAPTION, VAL_SPACE_2, VAL_SPACE_3,
VAL_SPACE_4, Z_MODAL_PANEL,
ACCENT_PRIMARY, BG_ELEVATED, BG_ELEVATED_HI, BORDER_SUBTLE, HighContrastBorder, RADIUS_SM,
STATE_DANGER, TEXT_DISABLED, TEXT_PRIMARY, TEXT_SECONDARY, TYPE_BODY, TYPE_BODY_LG,
TYPE_CAPTION, VAL_SPACE_2, VAL_SPACE_3, VAL_SPACE_4, Z_MODAL_PANEL,
};
// ---------------------------------------------------------------------------
@@ -213,7 +214,14 @@ fn open_sync_setup_modal(
// Exclude SettingsPanel: the Connect button closes settings in the same
// frame it fires SyncConfigureRequestEvent, but Bevy despawns are deferred
// so the settings scrim still exists in the world during this system.
other_modal_scrims: Query<(), (With<ModalScrim>, Without<SyncSetupScreen>, Without<SettingsPanel>)>,
other_modal_scrims: Query<
(),
(
With<ModalScrim>,
Without<SyncSetupScreen>,
Without<SettingsPanel>,
),
>,
mut commands: Commands,
mut focused: ResMut<SyncFocusedField>,
font_res: Option<Res<FontResource>>,
@@ -237,7 +245,12 @@ fn handle_text_input(
screen: Query<(), With<SyncSetupScreen>>,
mut key_events: MessageReader<KeyboardInput>,
mut focused: ResMut<SyncFocusedField>,
mut fields: Query<(&SyncFieldKind, &mut SyncFieldBuffer, &mut Text, &mut TextColor)>,
mut fields: Query<(
&SyncFieldKind,
&mut SyncFieldBuffer,
&mut Text,
&mut TextColor,
)>,
pending: Res<PendingAuthTask>,
) {
if screen.is_empty() || pending.task.is_some() {
@@ -316,12 +329,8 @@ fn handle_auth_button(
mut error_nodes: Query<(&mut Text, &mut TextColor), With<SyncAuthError>>,
mut busy_nodes: Query<&mut Visibility, With<SyncBusyOverlay>>,
) {
let login_clicked = login_q
.iter()
.any(|i| *i == Interaction::Pressed);
let register_clicked = register_q
.iter()
.any(|i| *i == Interaction::Pressed);
let login_clicked = login_q.iter().any(|i| *i == Interaction::Pressed);
let register_clicked = register_q.iter().any(|i| *i == Interaction::Pressed);
if !login_clicked && !register_clicked {
return;
@@ -505,8 +514,8 @@ fn handle_cancel(
screen: Query<Entity, With<SyncSetupScreen>>,
mut commands: Commands,
) {
let cancelled = cancel_q.iter().any(|i| *i == Interaction::Pressed)
|| keys.just_pressed(KeyCode::Escape);
let cancelled =
cancel_q.iter().any(|i| *i == Interaction::Pressed) || keys.just_pressed(KeyCode::Escape);
if !cancelled || screen.is_empty() {
return;
}
@@ -582,8 +591,8 @@ fn handle_delete_cancel(
screen: Query<Entity, With<DeleteConfirmScreen>>,
mut commands: Commands,
) {
let cancelled = cancel_q.iter().any(|i| *i == Interaction::Pressed)
|| keys.just_pressed(KeyCode::Escape);
let cancelled =
cancel_q.iter().any(|i| *i == Interaction::Pressed) || keys.just_pressed(KeyCode::Escape);
if !cancelled || screen.is_empty() {
return;
}
@@ -610,9 +619,9 @@ fn handle_delete_confirm(
}
let provider = provider.0.clone();
let rt = rt.0.clone();
pending.0 = Some(AsyncComputeTaskPool::get().spawn(async move {
rt.block_on(provider.delete_account())
}));
pending.0 = Some(
AsyncComputeTaskPool::get().spawn(async move { rt.block_on(provider.delete_account()) }),
);
}
/// Polls the in-flight delete-account task. On success fires `SyncLogoutRequestEvent`.
@@ -677,7 +686,7 @@ fn spawn_sync_setup_modal(commands: &mut Commands, font_res: Option<&FontResourc
SyncFieldKind::Url,
"Server URL",
"https://your-server.example.com",
true, // focused initially
true, // focused initially
font_res,
);
spawn_field(
@@ -783,7 +792,11 @@ fn spawn_field(
..default()
},
BackgroundColor(BG_ELEVATED),
BorderColor::all(if focused { ACCENT_PRIMARY } else { BORDER_SUBTLE }),
BorderColor::all(if focused {
ACCENT_PRIMARY
} else {
BORDER_SUBTLE
}),
HighContrastBorder::with_default(BORDER_SUBTLE),
))
.with_children(|border| {
@@ -806,7 +819,11 @@ fn spawn_action_button<M: Component>(
primary: bool,
font_res: Option<&FontResource>,
) {
let bg = if primary { ACCENT_PRIMARY } else { BG_ELEVATED_HI };
let bg = if primary {
ACCENT_PRIMARY
} else {
BG_ELEVATED_HI
};
let fg = TEXT_PRIMARY;
parent
.spawn((
@@ -820,7 +837,11 @@ fn spawn_action_button<M: Component>(
..default()
},
BackgroundColor(bg),
BorderColor::all(if primary { ACCENT_PRIMARY } else { BORDER_SUBTLE }),
BorderColor::all(if primary {
ACCENT_PRIMARY
} else {
BORDER_SUBTLE
}),
))
.with_children(|b| {
b.spawn((