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
pull/1/head
SYED MUSTUFA AHMED NAQVI 2026-02-11 12:47:53 +05:00
parent 35161336d0
commit 25a808907d
7 changed files with 86 additions and 16 deletions

3
.gitignore vendored
View File

@ -15,6 +15,9 @@ obj/
# Logs
*.log
# Local config (contains secrets)
appsettings.json
# SQLite database files (local runtime data)
*.db
*.db-shm

View File

@ -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);

View File

@ -174,7 +174,11 @@ public class ConfigService : IConfigService
SaveConfig();
}
public string GetMySqlConnectionString() => _config.MySqlConnectionString ?? string.Empty;
/// <summary>
/// Returns MySQL connection string for sync. Set via appsettings.json (copy from appsettings.example.json).
/// No default credentials; returns empty if not configured.
/// </summary>
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;
}
}

View File

@ -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<ScanRecord> 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))";

View File

@ -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();

View File

@ -251,7 +251,7 @@
FontSize="14"
Foreground="{StaticResource MutedText}"
Margin="0,0,0,20" />-->
<TextBlock Text="Sync API Endpoint (UIND)"
<TextBlock Text="Sync Data To (UIND)"
FontSize="16"
Foreground="{StaticResource PrimaryText}"
FontWeight="SemiBold" />
@ -263,9 +263,12 @@
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0"
Text="{Binding SyncApiEndpoint, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource ModernTextBoxStyle}" />
<TextBlock Grid.Column="0"
Text="Post data to production"
FontSize="18"
Foreground="{StaticResource PrimaryText}"
VerticalAlignment="Center"
Margin="4,0,0,0" />
<!-- Post button right next to textbox -->
<Button Grid.Column="2"
@ -276,7 +279,7 @@
IsEnabled="{Binding CanPostNow}" />
</Grid>
<TextBlock Text="Endpoint used to sync scan records."
<TextBlock Text=""
FontSize="14"
Foreground="{StaticResource MutedText}"
Margin="0,0,0,20" />
@ -361,7 +364,7 @@
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<WrapPanel Grid.Column="1" HorizontalAlignment="Right">
<Button Content="Cancel"
<Button Content="Back"
Command="{Binding BackCommand}"
Style="{StaticResource OutlineButtonStyle}"
MinWidth="120"

13
appsettings.example.json Normal file
View File

@ -0,0 +1,13 @@
{
"SyncApiEndpoint": "https://api.example.com/uind/sync",
"ScannerConnected": false,
"ScanTimeoutSeconds": 60,
"AdminCardId": "ADMIN",
"SiteId": "02",
"DeviceId": "",
"RememberAdminCredentials": false,
"SavedAdminUsername": "",
"SavedAdminPasswordProtected": "",
"MySqlConnectionString": "Server=localhost;Database=hrms;User=root;Password=CHANGE_ME;Port=3306;"
}