feat(engine): Phase M — sync transparency + local data backup
Closes out the 13-phase menu redesign. Three parts:
1. Post-sync merge summary: the pull poller now keeps the
`ConflictReport`s the merge produces (previously discarded) in a new
`SyncConflictLog` resource and toasts "Synced — N conflicts, kept
newer values" when a merge wasn't clean. No wire changes — the
server-side `SyncResponse.conflicts` field already existed.
2. Account tab detail: a per-field conflict list under the sync row
("win_streak_current — this device: 3 / server: 5") plus a
clean-merge caption, so the last merge is always inspectable.
3. Local data export/import: new `solitaire_data::transfer` module —
one versioned JSON bundle (settings, stats, achievements, progress)
written atomically next to the other saves, with a version-gated
loader that surfaces every failure instead of defaulting. Account
tab grows an Export/Import button pair (desktop+Android only); the
handler runs in `Last`, off the annotated Update spine. Import
rewrites the live resources, persists via the existing save fns,
and fires SettingsChangedEvent so appliers react normally.
Tests: bundle round-trip/version-gate/missing-file in solitaire_data;
ECS-level export→import round trip and failed-import-warns in
settings_plugin. Gate: workspace tests green, clippy -D warnings clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,7 @@ use crate::events::{
|
||||
use crate::resources::SettingsScrollPos;
|
||||
use crate::theme::ThemeThumbnailPair;
|
||||
|
||||
mod data_transfer;
|
||||
mod input;
|
||||
mod ui;
|
||||
mod updates;
|
||||
@@ -124,6 +125,12 @@ struct ThemeText;
|
||||
#[derive(Component, Debug)]
|
||||
struct SyncStatusText;
|
||||
|
||||
/// Marks the headline `Text` node of the last-merge summary in the
|
||||
/// Account tab (Phase M — sync transparency). Tests query it to assert
|
||||
/// the summary rendered.
|
||||
#[derive(Component, Debug)]
|
||||
pub(super) struct SyncMergeSummaryText;
|
||||
|
||||
/// Marks the `Text` node showing the active card-back index.
|
||||
#[derive(Component, Debug)]
|
||||
struct CardBackText;
|
||||
@@ -334,6 +341,10 @@ enum SettingsButton {
|
||||
DisconnectSync,
|
||||
/// Open the account-deletion confirmation modal.
|
||||
DeleteAccount,
|
||||
/// Write every JSON save into one backup bundle (Phase M).
|
||||
ExportData,
|
||||
/// Restore every JSON save from the backup bundle (Phase M).
|
||||
ImportData,
|
||||
Done,
|
||||
/// Select a specific card-back by index from the picker row.
|
||||
SelectCardBack(usize),
|
||||
@@ -399,6 +410,8 @@ impl SettingsButton {
|
||||
SettingsButton::ConnectSync => 91,
|
||||
SettingsButton::DisconnectSync => 92,
|
||||
SettingsButton::DeleteAccount => 93,
|
||||
SettingsButton::ExportData => 94,
|
||||
SettingsButton::ImportData => 95,
|
||||
// Done is tagged by `attach_focusable_to_modal_buttons` and
|
||||
// never reaches `attach_focusable_to_settings_buttons`; the
|
||||
// value here is only a fallback for completeness.
|
||||
@@ -452,6 +465,15 @@ impl Plugin for SettingsPlugin {
|
||||
.add_message::<SyncConfigureRequestEvent>()
|
||||
.add_message::<SyncLogoutRequestEvent>()
|
||||
.add_message::<DeleteAccountRequestEvent>()
|
||||
.add_message::<crate::events::DataExportRequestEvent>()
|
||||
.add_message::<crate::events::DataImportRequestEvent>()
|
||||
// Idempotent — the sync plugin also registers it; needed here
|
||||
// so the transfer handler's failure toasts work headless.
|
||||
.add_message::<crate::events::WarningToastEvent>()
|
||||
// Bulk backup/restore lives in Last: the export snapshot is
|
||||
// taken post-frame and an import's rewrites surface next
|
||||
// frame — keeps the rare path off the annotated Update spine.
|
||||
.add_systems(Last, data_transfer::handle_data_transfer)
|
||||
.add_message::<ToggleSettingsRequestEvent>()
|
||||
.add_message::<crate::events::ThemeStoreOpenRequestEvent>()
|
||||
.add_message::<InfoToastEvent>()
|
||||
|
||||
Reference in New Issue
Block a user