Files
OTSSignsOrchestrator/OTSSignsOrchestrator.Desktop/ViewModels/SettingsViewModel.cs
Matt Batchelder 45c94b6536
Some checks failed
Build and Publish Docker Image / build-and-push (push) Has been cancelled
feat: Add main application views and structure
- Implemented CreateInstanceView for creating new instances.
- Added HostsView for managing SSH hosts with CRUD operations.
- Created InstancesView for displaying and managing instances.
- Developed LogsView for viewing operation logs.
- Introduced SecretsView for managing secrets associated with hosts.
- Established SettingsView for configuring application settings.
- Created MainWindow as the main application window with navigation.
- Added app manifest and configuration files for logging and settings.
2026-02-18 10:43:27 -05:00

249 lines
14 KiB
C#

using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.DependencyInjection;
using OTSSignsOrchestrator.Core.Services;
namespace OTSSignsOrchestrator.Desktop.ViewModels;
/// <summary>
/// ViewModel for the Settings page — manages Git, MySQL, SMTP, Pangolin, CIFS,
/// and Instance Defaults configuration, persisted via SettingsService.
/// </summary>
public partial class SettingsViewModel : ObservableObject
{
private readonly IServiceProvider _services;
[ObservableProperty] private string _statusMessage = string.Empty;
[ObservableProperty] private bool _isBusy;
// ── Git ──────────────────────────────────────────────────────────────────
[ObservableProperty] private string _gitRepoUrl = string.Empty;
[ObservableProperty] private string _gitRepoPat = string.Empty;
// ── MySQL Admin ─────────────────────────────────────────────────────────
[ObservableProperty] private string _mySqlHost = string.Empty;
[ObservableProperty] private string _mySqlPort = "3306";
[ObservableProperty] private string _mySqlAdminUser = string.Empty;
[ObservableProperty] private string _mySqlAdminPassword = string.Empty;
// ── SMTP ────────────────────────────────────────────────────────────────
[ObservableProperty] private string _smtpServer = string.Empty;
[ObservableProperty] private string _smtpUsername = string.Empty;
[ObservableProperty] private string _smtpPassword = string.Empty;
[ObservableProperty] private bool _smtpUseTls = true;
[ObservableProperty] private bool _smtpUseStartTls = true;
[ObservableProperty] private string _smtpRewriteDomain = string.Empty;
[ObservableProperty] private string _smtpHostname = string.Empty;
[ObservableProperty] private string _smtpFromLineOverride = "NO";
// ── Pangolin ────────────────────────────────────────────────────────────
[ObservableProperty] private string _pangolinEndpoint = "https://app.pangolin.net";
// ── CIFS ────────────────────────────────────────────────────────────────
[ObservableProperty] private string _cifsServer = string.Empty;
[ObservableProperty] private string _cifsShareBasePath = string.Empty;
[ObservableProperty] private string _cifsUsername = string.Empty;
[ObservableProperty] private string _cifsPassword = string.Empty;
[ObservableProperty] private string _cifsOptions = "file_mode=0777,dir_mode=0777";
// ── Instance Defaults ───────────────────────────────────────────────────
[ObservableProperty] private string _defaultCmsImage = "ghcr.io/xibosignage/xibo-cms:release-4.2.3";
[ObservableProperty] private string _defaultNewtImage = "fosrl/newt";
[ObservableProperty] private string _defaultMemcachedImage = "memcached:alpine";
[ObservableProperty] private string _defaultQuickChartImage = "ianw/quickchart";
[ObservableProperty] private string _defaultCmsServerNameTemplate = "{abbrev}.ots-signs.com";
[ObservableProperty] private string _defaultThemeHostPath = "/cms/{abbrev}-cms-theme-custom";
[ObservableProperty] private string _defaultMySqlDbTemplate = "{abbrev}_cms_db";
[ObservableProperty] private string _defaultMySqlUserTemplate = "{abbrev}_cms";
[ObservableProperty] private string _defaultPhpPostMaxSize = "10G";
[ObservableProperty] private string _defaultPhpUploadMaxFilesize = "10G";
[ObservableProperty] private string _defaultPhpMaxExecutionTime = "600";
public SettingsViewModel(IServiceProvider services)
{
_services = services;
_ = LoadAsync();
}
[RelayCommand]
private async Task LoadAsync()
{
IsBusy = true;
try
{
using var scope = _services.CreateScope();
var svc = scope.ServiceProvider.GetRequiredService<SettingsService>();
// Git
GitRepoUrl = await svc.GetAsync(SettingsService.GitRepoUrl, string.Empty);
GitRepoPat = await svc.GetAsync(SettingsService.GitRepoPat, string.Empty);
// MySQL
MySqlHost = await svc.GetAsync(SettingsService.MySqlHost, string.Empty);
MySqlPort = await svc.GetAsync(SettingsService.MySqlPort, "3306");
MySqlAdminUser = await svc.GetAsync(SettingsService.MySqlAdminUser, string.Empty);
MySqlAdminPassword = await svc.GetAsync(SettingsService.MySqlAdminPassword, string.Empty);
// SMTP
SmtpServer = await svc.GetAsync(SettingsService.SmtpServer, string.Empty);
SmtpUsername = await svc.GetAsync(SettingsService.SmtpUsername, string.Empty);
SmtpPassword = await svc.GetAsync(SettingsService.SmtpPassword, string.Empty);
SmtpUseTls = (await svc.GetAsync(SettingsService.SmtpUseTls, "YES")) == "YES";
SmtpUseStartTls = (await svc.GetAsync(SettingsService.SmtpUseStartTls, "YES")) == "YES";
SmtpRewriteDomain = await svc.GetAsync(SettingsService.SmtpRewriteDomain, string.Empty);
SmtpHostname = await svc.GetAsync(SettingsService.SmtpHostname, string.Empty);
SmtpFromLineOverride = await svc.GetAsync(SettingsService.SmtpFromLineOverride, "NO");
// Pangolin
PangolinEndpoint = await svc.GetAsync(SettingsService.PangolinEndpoint, "https://app.pangolin.net");
// CIFS
CifsServer = await svc.GetAsync(SettingsService.CifsServer, string.Empty);
CifsShareBasePath = await svc.GetAsync(SettingsService.CifsShareBasePath, string.Empty);
CifsUsername = await svc.GetAsync(SettingsService.CifsUsername, string.Empty);
CifsPassword = await svc.GetAsync(SettingsService.CifsPassword, string.Empty);
CifsOptions = await svc.GetAsync(SettingsService.CifsOptions, "file_mode=0777,dir_mode=0777");
// Instance Defaults
DefaultCmsImage = await svc.GetAsync(SettingsService.DefaultCmsImage, "ghcr.io/xibosignage/xibo-cms:release-4.2.3");
DefaultNewtImage = await svc.GetAsync(SettingsService.DefaultNewtImage, "fosrl/newt");
DefaultMemcachedImage = await svc.GetAsync(SettingsService.DefaultMemcachedImage, "memcached:alpine");
DefaultQuickChartImage = await svc.GetAsync(SettingsService.DefaultQuickChartImage, "ianw/quickchart");
DefaultCmsServerNameTemplate = await svc.GetAsync(SettingsService.DefaultCmsServerNameTemplate, "{abbrev}.ots-signs.com");
DefaultThemeHostPath = await svc.GetAsync(SettingsService.DefaultThemeHostPath, "/cms/{abbrev}-cms-theme-custom");
DefaultMySqlDbTemplate = await svc.GetAsync(SettingsService.DefaultMySqlDbTemplate, "{abbrev}_cms_db");
DefaultMySqlUserTemplate = await svc.GetAsync(SettingsService.DefaultMySqlUserTemplate, "{abbrev}_cms");
DefaultPhpPostMaxSize = await svc.GetAsync(SettingsService.DefaultPhpPostMaxSize, "10G");
DefaultPhpUploadMaxFilesize = await svc.GetAsync(SettingsService.DefaultPhpUploadMaxFilesize, "10G");
DefaultPhpMaxExecutionTime = await svc.GetAsync(SettingsService.DefaultPhpMaxExecutionTime, "600");
StatusMessage = "Settings loaded.";
}
catch (Exception ex)
{
StatusMessage = $"Error loading settings: {ex.Message}";
}
finally
{
IsBusy = false;
}
}
[RelayCommand]
private async Task SaveAsync()
{
IsBusy = true;
try
{
using var scope = _services.CreateScope();
var svc = scope.ServiceProvider.GetRequiredService<SettingsService>();
var settings = new List<(string Key, string? Value, string Category, bool IsSensitive)>
{
// Git
(SettingsService.GitRepoUrl, NullIfEmpty(GitRepoUrl), SettingsService.CatGit, false),
(SettingsService.GitRepoPat, NullIfEmpty(GitRepoPat), SettingsService.CatGit, true),
// MySQL
(SettingsService.MySqlHost, NullIfEmpty(MySqlHost), SettingsService.CatMySql, false),
(SettingsService.MySqlPort, MySqlPort, SettingsService.CatMySql, false),
(SettingsService.MySqlAdminUser, NullIfEmpty(MySqlAdminUser), SettingsService.CatMySql, false),
(SettingsService.MySqlAdminPassword, NullIfEmpty(MySqlAdminPassword), SettingsService.CatMySql, true),
// SMTP
(SettingsService.SmtpServer, NullIfEmpty(SmtpServer), SettingsService.CatSmtp, false),
(SettingsService.SmtpUsername, NullIfEmpty(SmtpUsername), SettingsService.CatSmtp, false),
(SettingsService.SmtpPassword, NullIfEmpty(SmtpPassword), SettingsService.CatSmtp, true),
(SettingsService.SmtpUseTls, SmtpUseTls ? "YES" : "NO", SettingsService.CatSmtp, false),
(SettingsService.SmtpUseStartTls, SmtpUseStartTls ? "YES" : "NO", SettingsService.CatSmtp, false),
(SettingsService.SmtpRewriteDomain, NullIfEmpty(SmtpRewriteDomain), SettingsService.CatSmtp, false),
(SettingsService.SmtpHostname, NullIfEmpty(SmtpHostname), SettingsService.CatSmtp, false),
(SettingsService.SmtpFromLineOverride, SmtpFromLineOverride, SettingsService.CatSmtp, false),
// Pangolin
(SettingsService.PangolinEndpoint, NullIfEmpty(PangolinEndpoint), SettingsService.CatPangolin, false),
// CIFS
(SettingsService.CifsServer, NullIfEmpty(CifsServer), SettingsService.CatCifs, false),
(SettingsService.CifsShareBasePath, NullIfEmpty(CifsShareBasePath), SettingsService.CatCifs, false),
(SettingsService.CifsUsername, NullIfEmpty(CifsUsername), SettingsService.CatCifs, false),
(SettingsService.CifsPassword, NullIfEmpty(CifsPassword), SettingsService.CatCifs, true),
(SettingsService.CifsOptions, NullIfEmpty(CifsOptions), SettingsService.CatCifs, false),
// Instance Defaults
(SettingsService.DefaultCmsImage, DefaultCmsImage, SettingsService.CatDefaults, false),
(SettingsService.DefaultNewtImage, DefaultNewtImage, SettingsService.CatDefaults, false),
(SettingsService.DefaultMemcachedImage, DefaultMemcachedImage, SettingsService.CatDefaults, false),
(SettingsService.DefaultQuickChartImage, DefaultQuickChartImage, SettingsService.CatDefaults, false),
(SettingsService.DefaultCmsServerNameTemplate, DefaultCmsServerNameTemplate, SettingsService.CatDefaults, false),
(SettingsService.DefaultThemeHostPath, DefaultThemeHostPath, SettingsService.CatDefaults, false),
(SettingsService.DefaultMySqlDbTemplate, DefaultMySqlDbTemplate, SettingsService.CatDefaults, false),
(SettingsService.DefaultMySqlUserTemplate, DefaultMySqlUserTemplate, SettingsService.CatDefaults, false),
(SettingsService.DefaultPhpPostMaxSize, DefaultPhpPostMaxSize, SettingsService.CatDefaults, false),
(SettingsService.DefaultPhpUploadMaxFilesize, DefaultPhpUploadMaxFilesize, SettingsService.CatDefaults, false),
(SettingsService.DefaultPhpMaxExecutionTime, DefaultPhpMaxExecutionTime, SettingsService.CatDefaults, false),
};
await svc.SaveManyAsync(settings);
StatusMessage = "Settings saved successfully.";
}
catch (Exception ex)
{
StatusMessage = $"Error saving settings: {ex.Message}";
}
finally
{
IsBusy = false;
}
}
[RelayCommand]
private async Task TestMySqlConnectionAsync()
{
if (string.IsNullOrWhiteSpace(MySqlHost) || string.IsNullOrWhiteSpace(MySqlAdminUser))
{
StatusMessage = "MySQL Host and Admin User are required for connection test.";
return;
}
IsBusy = true;
StatusMessage = "Testing MySQL connection via SSH...";
try
{
// The test runs a mysql --version or a simple SELECT 1 query via SSH
// We need an SshHost to route through — use the first available
using var scope = _services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<OTSSignsOrchestrator.Core.Data.XiboContext>();
var host = await Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions
.FirstOrDefaultAsync(db.SshHosts);
if (host == null)
{
StatusMessage = "No SSH hosts configured. Add one in the Hosts page first.";
return;
}
var ssh = _services.GetRequiredService<Services.SshConnectionService>();
var port = int.TryParse(MySqlPort, out var p) ? p : 3306;
var cmd = $"mysql -h {MySqlHost} -P {port} -u {MySqlAdminUser} -p'{MySqlAdminPassword.Replace("'", "'\\''")}' -e 'SELECT 1' 2>&1";
var (exitCode, stdout, stderr) = await ssh.RunCommandAsync(host, cmd);
StatusMessage = exitCode == 0
? $"MySQL connection successful via {host.Label}."
: $"MySQL connection failed: {(string.IsNullOrWhiteSpace(stderr) ? stdout : stderr).Trim()}";
}
catch (Exception ex)
{
StatusMessage = $"MySQL test error: {ex.Message}";
}
finally
{
IsBusy = false;
}
}
private static string? NullIfEmpty(string? value)
=> string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}