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 Backpull/1/head
parent
35161336d0
commit
25a808907d
|
|
@ -15,6 +15,9 @@ obj/
|
|||
# Logs
|
||||
*.log
|
||||
|
||||
# Local config (contains secrets)
|
||||
appsettings.json
|
||||
|
||||
# SQLite database files (local runtime data)
|
||||
*.db
|
||||
*.db-shm
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))";
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;"
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue