From 25a808907da4a61870535d210bdd30fb614709a4 Mon Sep 17 00:00:00 2001 From: "mustafa.ahmed" Date: Wed, 11 Feb 2026 12:47:53 +0500 Subject: [PATCH] feat(settings): add manual sync + secure config template Add Settings PostDataNow action via ISyncService Remove hardcoded MySQL fallback; skip sync when not configured Use production snake_case columns + always run cleanup Add appsettings.example.json; ignore appsettings.json to avoid secrets Rename Settings Cancel button to Back --- .gitignore | 3 +++ App.xaml.cs | 2 +- Services/ConfigService.cs | 8 ++++-- Services/SyncService.cs | 12 ++++++--- ViewModels/SettingsViewModel.cs | 43 ++++++++++++++++++++++++++++++++- Views/SettingsView.xaml | 21 +++++++++------- appsettings.example.json | 13 ++++++++++ 7 files changed, 86 insertions(+), 16 deletions(-) create mode 100644 appsettings.example.json diff --git a/.gitignore b/.gitignore index 25496a5..551f5be 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ obj/ # Logs *.log +# Local config (contains secrets) +appsettings.json + # SQLite database files (local runtime data) *.db *.db-shm diff --git a/App.xaml.cs b/App.xaml.cs index 67bf3d6..a5754e4 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -44,7 +44,7 @@ public partial class App : Application () => new ScannerDashboardViewModel(rfidService, navigationService, session, configService), () => new MainDashboardViewModel(navigationService, rfidService, configService, session), () => new AdminSettingsAuthViewModel(authService, session, navigationService, configService), - () => new SettingsViewModel(configService, navigationService, adminAuditService)); + () => new SettingsViewModel(configService, navigationService, adminAuditService, syncService)); var mainViewModel = new MainViewModel(navigationService); diff --git a/Services/ConfigService.cs b/Services/ConfigService.cs index 5458681..cda21f3 100644 --- a/Services/ConfigService.cs +++ b/Services/ConfigService.cs @@ -174,7 +174,11 @@ public class ConfigService : IConfigService SaveConfig(); } - public string GetMySqlConnectionString() => _config.MySqlConnectionString ?? string.Empty; + /// + /// Returns MySQL connection string for sync. Set via appsettings.json (copy from appsettings.example.json). + /// No default credentials; returns empty if not configured. + /// + public string GetMySqlConnectionString() => (_config.MySqlConnectionString ?? string.Empty).Trim(); public void SetMySqlConnectionString(string connectionString) { @@ -238,7 +242,7 @@ public class ConfigService : IConfigService public string SavedAdminUsername { get; set; } = string.Empty; public string SavedAdminPasswordProtected { get; set; } = string.Empty; - // MySQL connection string for direct DB sync (local testing). + // MySQL connection string for sync. Set in appsettings.json (see appsettings.example.json). No default. public string MySqlConnectionString { get; set; } = string.Empty; } } diff --git a/Services/SyncService.cs b/Services/SyncService.cs index 5c398dd..d515b6e 100644 --- a/Services/SyncService.cs +++ b/Services/SyncService.cs @@ -23,9 +23,11 @@ public class SyncService : ISyncService public async Task SyncNowAsync(CancellationToken cancellationToken = default) { var connectionString = _configService.GetMySqlConnectionString(); - // TODO: For local testing only. Set MySqlConnectionString in appsettings.json or via config to avoid hardcoding. if (string.IsNullOrWhiteSpace(connectionString)) - connectionString = "Server=localhost;Database=canteen_prod;User=root;Password=Root@12345_;Port=3306;"; + { + System.Diagnostics.Debug.WriteLine("MySQL connection string not configured; skipping sync."); + return; + } List toSync; using (var db = _dbFactory.CreateDbContext()) @@ -37,6 +39,9 @@ public class SyncService : ISyncService .ConfigureAwait(false); } + // Always run day-end cleanup (remove synced rows from previous days), even when there's nothing to sync. + await CleanupOldSyncedRowsAsync(cancellationToken).ConfigureAwait(false); + if (toSync.Count == 0) return; @@ -52,9 +57,10 @@ public class SyncService : ISyncService { // Use INSERT IGNORE to handle duplicates (unique key on DeviceId, DeviceLocalRowId) // This ensures no duplicates even if sync runs multiple times + // Column names match production: hrms.lunch_order_transactions (snake_case) var insertSql = @" INSERT IGNORE INTO lunch_order_transactions - (DeviceLocalRowId, ScanTimeUtc, SiteId, DeviceId, CardId, IpAddress, ReceivedAtUtc) + (device_local_row_id, scan_time_utc, site_id, device_id, card_id, ip_address, received_at_utc) VALUES (@DeviceLocalRowId, @ScanTimeUtc, @SiteId, @DeviceId, @CardId, @IpAddress, UTC_TIMESTAMP(3))"; diff --git a/ViewModels/SettingsViewModel.cs b/ViewModels/SettingsViewModel.cs index 99c0b8b..df728c7 100644 --- a/ViewModels/SettingsViewModel.cs +++ b/ViewModels/SettingsViewModel.cs @@ -12,6 +12,7 @@ public partial class SettingsViewModel : ObservableObject private readonly IConfigService _configService; private readonly INavigationService _navigation; private readonly IAdminAuditService _adminAudit; + private readonly ISyncService _syncService; [ObservableProperty] private string _syncApiEndpoint = string.Empty; @@ -44,11 +45,16 @@ public partial class SettingsViewModel : ObservableObject OnPropertyChanged(nameof(CanPostNow)); } - public SettingsViewModel(IConfigService configService, INavigationService navigation, IAdminAuditService adminAudit) + public SettingsViewModel( + IConfigService configService, + INavigationService navigation, + IAdminAuditService adminAudit, + ISyncService syncService) { _configService = configService; _navigation = navigation; _adminAudit = adminAudit; + _syncService = syncService; LoadFromConfig(); } @@ -92,6 +98,41 @@ public partial class SettingsViewModel : ObservableObject _navigation.NavigateBackFromSettings(GetDashboardTimeout()); } + [RelayCommand] + private async Task PostDataNow() + { + if (IsPosting) + return; + + SaveMessage = string.Empty; + IsError = false; + IsPosting = true; + + try + { + // If not configured, do not attempt sync. + if (string.IsNullOrWhiteSpace(_configService.GetMySqlConnectionString())) + { + SaveMessage = "MySQL connection string not configured."; + IsError = true; + return; + } + + await _syncService.SyncNowAsync().ConfigureAwait(false); + SaveMessage = "Posted data to production."; + IsError = false; + } + catch + { + SaveMessage = "Failed to post data to production."; + IsError = true; + } + finally + { + IsPosting = false; + } + } + private TimeSpan GetDashboardTimeout() { var seconds = _configService.GetScanTimeoutSeconds(); diff --git a/Views/SettingsView.xaml b/Views/SettingsView.xaml index 1c54146..8be04b4 100644 --- a/Views/SettingsView.xaml +++ b/Views/SettingsView.xaml @@ -251,7 +251,7 @@ FontSize="14" Foreground="{StaticResource MutedText}" Margin="0,0,0,20" />--> - @@ -263,9 +263,12 @@ - +