2026-02-18 10:43:27 -05:00
|
|
|
using System.Collections.ObjectModel;
|
2026-02-19 08:27:54 -05:00
|
|
|
using Avalonia;
|
|
|
|
|
using Avalonia.Controls;
|
|
|
|
|
using Avalonia.Controls.ApplicationLifetimes;
|
2026-02-18 10:43:27 -05:00
|
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
|
|
|
using CommunityToolkit.Mvvm.Input;
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
|
using OTSSignsOrchestrator.Core.Data;
|
|
|
|
|
using OTSSignsOrchestrator.Core.Models.DTOs;
|
|
|
|
|
using OTSSignsOrchestrator.Core.Models.Entities;
|
|
|
|
|
using OTSSignsOrchestrator.Core.Services;
|
|
|
|
|
using OTSSignsOrchestrator.Desktop.Services;
|
|
|
|
|
|
|
|
|
|
namespace OTSSignsOrchestrator.Desktop.ViewModels;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// ViewModel for the Create Instance form.
|
|
|
|
|
/// Simplified: Customer Name, Abbreviation, SSH Host, and optional Newt credentials.
|
|
|
|
|
/// All other config comes from the Settings page.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public partial class CreateInstanceViewModel : ObservableObject
|
|
|
|
|
{
|
|
|
|
|
private readonly IServiceProvider _services;
|
2026-02-25 17:39:17 -05:00
|
|
|
private readonly MainWindowViewModel _mainVm;
|
2026-02-18 10:43:27 -05:00
|
|
|
|
|
|
|
|
[ObservableProperty] private string _statusMessage = string.Empty;
|
|
|
|
|
[ObservableProperty] private bool _isBusy;
|
|
|
|
|
[ObservableProperty] private string _deployOutput = string.Empty;
|
|
|
|
|
[ObservableProperty] private double _progressPercent;
|
|
|
|
|
[ObservableProperty] private string _progressStep = string.Empty;
|
|
|
|
|
|
|
|
|
|
// Core form fields — only these two are required from the user
|
|
|
|
|
[ObservableProperty] private string _customerName = string.Empty;
|
|
|
|
|
[ObservableProperty] private string _customerAbbrev = string.Empty;
|
|
|
|
|
|
|
|
|
|
// Optional Pangolin/Newt credentials (per-instance)
|
|
|
|
|
[ObservableProperty] private string _newtId = string.Empty;
|
|
|
|
|
[ObservableProperty] private string _newtSecret = string.Empty;
|
|
|
|
|
|
2026-02-19 08:27:54 -05:00
|
|
|
// NFS volume settings (per-instance, defaults loaded from global settings)
|
|
|
|
|
[ObservableProperty] private string _nfsServer = string.Empty;
|
|
|
|
|
[ObservableProperty] private string _nfsExport = string.Empty;
|
|
|
|
|
[ObservableProperty] private string _nfsExportFolder = string.Empty;
|
|
|
|
|
[ObservableProperty] private string _nfsExtraOptions = string.Empty;
|
2026-02-18 10:43:27 -05:00
|
|
|
|
2026-02-25 17:39:17 -05:00
|
|
|
/// <summary>When enabled, existing Docker volumes for the stack are removed before deploying.</summary>
|
|
|
|
|
[ObservableProperty] private bool _purgeStaleVolumes = false;
|
|
|
|
|
|
2026-02-18 10:43:27 -05:00
|
|
|
// SSH host selection
|
|
|
|
|
[ObservableProperty] private ObservableCollection<SshHost> _availableHosts = new();
|
|
|
|
|
[ObservableProperty] private SshHost? _selectedSshHost;
|
|
|
|
|
|
2026-02-19 08:27:54 -05:00
|
|
|
// YML preview
|
|
|
|
|
[ObservableProperty] private string _previewYml = string.Empty;
|
|
|
|
|
[ObservableProperty] private bool _isLoadingYml;
|
|
|
|
|
|
|
|
|
|
public bool HasPreviewYml => !string.IsNullOrEmpty(PreviewYml);
|
|
|
|
|
partial void OnPreviewYmlChanged(string value) => OnPropertyChanged(nameof(HasPreviewYml));
|
|
|
|
|
|
2026-02-18 10:43:27 -05:00
|
|
|
// ── Derived preview properties ───────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
public string PreviewStackName => Valid ? $"{Abbrev}-cms-stack" : "—";
|
|
|
|
|
public string PreviewServiceWeb => Valid ? $"{Abbrev}-web" : "—";
|
|
|
|
|
public string PreviewServiceCache => Valid ? $"{Abbrev}-memcached" : "—";
|
|
|
|
|
public string PreviewServiceChart => Valid ? $"{Abbrev}-quickchart" : "—";
|
|
|
|
|
public string PreviewServiceNewt => Valid ? $"{Abbrev}-newt" : "—";
|
|
|
|
|
public string PreviewNetwork => Valid ? $"{Abbrev}-net" : "—";
|
2026-02-19 08:27:54 -05:00
|
|
|
public string PreviewVolCustom => Valid ? $"{Abbrev}/cms-custom" : "—";
|
|
|
|
|
public string PreviewVolBackup => Valid ? $"{Abbrev}/cms-backup" : "—";
|
|
|
|
|
public string PreviewVolLibrary => Valid ? $"{Abbrev}/cms-library" : "—";
|
|
|
|
|
public string PreviewVolUserscripts => Valid ? $"{Abbrev}/cms-userscripts": "—";
|
|
|
|
|
public string PreviewVolCaCerts => Valid ? $"{Abbrev}/cms-ca-certs" : "—";
|
2026-02-18 10:43:27 -05:00
|
|
|
public string PreviewSecret => Valid ? $"{Abbrev}-cms-db-password": "—";
|
2026-02-19 08:27:54 -05:00
|
|
|
public string PreviewSecretUser => Valid ? $"{Abbrev}-cms-db-user" : "—";
|
|
|
|
|
public string PreviewSecretHost => "global_mysql_host";
|
|
|
|
|
public string PreviewSecretPort => "global_mysql_port";
|
2026-02-18 10:43:27 -05:00
|
|
|
public string PreviewMySqlDb => Valid ? $"{Abbrev}_cms_db" : "—";
|
2026-02-19 08:27:54 -05:00
|
|
|
public string PreviewMySqlUser => Valid ? $"{Abbrev}_cms_user" : "—";
|
2026-02-18 10:43:27 -05:00
|
|
|
public string PreviewCmsUrl => Valid ? $"https://{Abbrev}.ots-signs.com" : "—";
|
|
|
|
|
|
|
|
|
|
private string Abbrev => CustomerAbbrev.Trim().ToLowerInvariant();
|
|
|
|
|
private bool Valid => Abbrev.Length == 3 && System.Text.RegularExpressions.Regex.IsMatch(Abbrev, "^[a-z]{3}$");
|
|
|
|
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
2026-02-25 17:39:17 -05:00
|
|
|
public CreateInstanceViewModel(IServiceProvider services, MainWindowViewModel mainVm)
|
2026-02-18 10:43:27 -05:00
|
|
|
{
|
|
|
|
|
_services = services;
|
2026-02-25 17:39:17 -05:00
|
|
|
_mainVm = mainVm;
|
2026-02-18 10:43:27 -05:00
|
|
|
_ = LoadHostsAsync();
|
2026-02-19 08:27:54 -05:00
|
|
|
_ = LoadNfsDefaultsAsync();
|
2026-02-18 10:43:27 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
partial void OnCustomerAbbrevChanged(string value) => RefreshPreview();
|
|
|
|
|
|
|
|
|
|
private void RefreshPreview()
|
|
|
|
|
{
|
|
|
|
|
OnPropertyChanged(nameof(PreviewStackName));
|
|
|
|
|
OnPropertyChanged(nameof(PreviewServiceWeb));
|
|
|
|
|
OnPropertyChanged(nameof(PreviewServiceCache));
|
|
|
|
|
OnPropertyChanged(nameof(PreviewServiceChart));
|
|
|
|
|
OnPropertyChanged(nameof(PreviewServiceNewt));
|
|
|
|
|
OnPropertyChanged(nameof(PreviewNetwork));
|
|
|
|
|
OnPropertyChanged(nameof(PreviewVolCustom));
|
|
|
|
|
OnPropertyChanged(nameof(PreviewVolBackup));
|
|
|
|
|
OnPropertyChanged(nameof(PreviewVolLibrary));
|
|
|
|
|
OnPropertyChanged(nameof(PreviewVolUserscripts));
|
|
|
|
|
OnPropertyChanged(nameof(PreviewVolCaCerts));
|
|
|
|
|
OnPropertyChanged(nameof(PreviewSecret));
|
2026-02-19 08:27:54 -05:00
|
|
|
OnPropertyChanged(nameof(PreviewSecretUser));
|
|
|
|
|
OnPropertyChanged(nameof(PreviewSecretHost));
|
|
|
|
|
OnPropertyChanged(nameof(PreviewSecretPort));
|
2026-02-18 10:43:27 -05:00
|
|
|
OnPropertyChanged(nameof(PreviewMySqlDb));
|
|
|
|
|
OnPropertyChanged(nameof(PreviewMySqlUser));
|
|
|
|
|
OnPropertyChanged(nameof(PreviewCmsUrl));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task LoadHostsAsync()
|
|
|
|
|
{
|
|
|
|
|
using var scope = _services.CreateScope();
|
|
|
|
|
var db = scope.ServiceProvider.GetRequiredService<XiboContext>();
|
|
|
|
|
var hosts = await db.SshHosts.OrderBy(h => h.Label).ToListAsync();
|
|
|
|
|
AvailableHosts = new ObservableCollection<SshHost>(hosts);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 08:27:54 -05:00
|
|
|
private async Task LoadNfsDefaultsAsync()
|
2026-02-18 10:43:27 -05:00
|
|
|
{
|
|
|
|
|
using var scope = _services.CreateScope();
|
|
|
|
|
var settings = scope.ServiceProvider.GetRequiredService<SettingsService>();
|
2026-02-19 08:27:54 -05:00
|
|
|
NfsServer = await settings.GetAsync(SettingsService.NfsServer) ?? string.Empty;
|
|
|
|
|
NfsExport = await settings.GetAsync(SettingsService.NfsExport) ?? string.Empty;
|
|
|
|
|
NfsExportFolder = await settings.GetAsync(SettingsService.NfsExportFolder) ?? string.Empty;
|
|
|
|
|
NfsExtraOptions = await settings.GetAsync(SettingsService.NfsOptions) ?? string.Empty;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private async Task LoadYmlPreviewAsync()
|
|
|
|
|
{
|
|
|
|
|
if (!Valid)
|
|
|
|
|
{
|
|
|
|
|
PreviewYml = "# Abbreviation must be exactly 3 lowercase letters (a-z) before loading the YML preview.";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
IsLoadingYml = true;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
using var scope = _services.CreateScope();
|
|
|
|
|
var settings = scope.ServiceProvider.GetRequiredService<SettingsService>();
|
|
|
|
|
var git = scope.ServiceProvider.GetRequiredService<GitTemplateService>();
|
|
|
|
|
var composer = scope.ServiceProvider.GetRequiredService<ComposeRenderService>();
|
|
|
|
|
|
|
|
|
|
var repoUrl = await settings.GetAsync(SettingsService.GitRepoUrl);
|
|
|
|
|
var repoPat = await settings.GetAsync(SettingsService.GitRepoPat);
|
|
|
|
|
if (string.IsNullOrWhiteSpace(repoUrl))
|
|
|
|
|
{
|
|
|
|
|
PreviewYml = "# Git template repository URL is not configured. Set it in Settings → Git Repo URL.";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var templateConfig = await git.FetchAsync(repoUrl, repoPat);
|
|
|
|
|
|
|
|
|
|
var abbrev = Abbrev;
|
|
|
|
|
var stackName = $"{abbrev}-cms-stack";
|
|
|
|
|
|
|
|
|
|
var mySqlHost = await settings.GetAsync(SettingsService.MySqlHost, "localhost");
|
|
|
|
|
var mySqlPort = await settings.GetAsync(SettingsService.MySqlPort, "3306");
|
|
|
|
|
var mySqlDbName = (await settings.GetAsync(SettingsService.DefaultMySqlDbTemplate, "{abbrev}_cms_db")).Replace("{abbrev}", abbrev);
|
|
|
|
|
var mySqlUser = (await settings.GetAsync(SettingsService.DefaultMySqlUserTemplate, "{abbrev}_cms_user")).Replace("{abbrev}", abbrev);
|
|
|
|
|
|
2026-02-27 22:15:24 -05:00
|
|
|
var cmsServerName = (await settings.GetAsync(SettingsService.DefaultCmsServerNameTemplate, "app.ots-signs.com")).Replace("{abbrev}", abbrev);
|
2026-02-19 08:27:54 -05:00
|
|
|
var themePath = (await settings.GetAsync(SettingsService.DefaultThemeHostPath, "/cms/ots-theme")).Replace("{abbrev}", abbrev);
|
|
|
|
|
|
|
|
|
|
var smtpServer = await settings.GetAsync(SettingsService.SmtpServer, string.Empty);
|
|
|
|
|
var smtpUsername = await settings.GetAsync(SettingsService.SmtpUsername, string.Empty);
|
|
|
|
|
var smtpPassword = await settings.GetAsync(SettingsService.SmtpPassword, string.Empty);
|
|
|
|
|
var smtpUseTls = await settings.GetAsync(SettingsService.SmtpUseTls, "YES");
|
|
|
|
|
var smtpUseStartTls = await settings.GetAsync(SettingsService.SmtpUseStartTls, "YES");
|
|
|
|
|
var smtpRewriteDomain = await settings.GetAsync(SettingsService.SmtpRewriteDomain, string.Empty);
|
|
|
|
|
var smtpHostname = await settings.GetAsync(SettingsService.SmtpHostname, string.Empty);
|
|
|
|
|
var smtpFromLineOverride = await settings.GetAsync(SettingsService.SmtpFromLineOverride, "NO");
|
|
|
|
|
|
|
|
|
|
var pangolinEndpoint = await settings.GetAsync(SettingsService.PangolinEndpoint, "https://app.pangolin.net");
|
|
|
|
|
|
|
|
|
|
var cmsImage = await settings.GetAsync(SettingsService.DefaultCmsImage, "ghcr.io/xibosignage/xibo-cms:release-4.2.3");
|
|
|
|
|
var newtImage = await settings.GetAsync(SettingsService.DefaultNewtImage, "fosrl/newt");
|
|
|
|
|
var memcachedImage = await settings.GetAsync(SettingsService.DefaultMemcachedImage, "memcached:alpine");
|
|
|
|
|
var quickChartImage = await settings.GetAsync(SettingsService.DefaultQuickChartImage, "ianw/quickchart");
|
|
|
|
|
|
|
|
|
|
var phpPostMaxSize = await settings.GetAsync(SettingsService.DefaultPhpPostMaxSize, "10G");
|
|
|
|
|
var phpUploadMaxFilesize = await settings.GetAsync(SettingsService.DefaultPhpUploadMaxFilesize, "10G");
|
|
|
|
|
var phpMaxExecutionTime = await settings.GetAsync(SettingsService.DefaultPhpMaxExecutionTime, "600");
|
|
|
|
|
|
|
|
|
|
// Use form values; fall back to saved global settings
|
|
|
|
|
var nfsServer = string.IsNullOrWhiteSpace(NfsServer) ? await settings.GetAsync(SettingsService.NfsServer) : NfsServer;
|
|
|
|
|
var nfsExport = string.IsNullOrWhiteSpace(NfsExport) ? await settings.GetAsync(SettingsService.NfsExport) : NfsExport;
|
|
|
|
|
var nfsExportFolder = string.IsNullOrWhiteSpace(NfsExportFolder) ? await settings.GetAsync(SettingsService.NfsExportFolder) : NfsExportFolder;
|
|
|
|
|
var nfsOptions = string.IsNullOrWhiteSpace(NfsExtraOptions) ? await settings.GetAsync(SettingsService.NfsOptions, string.Empty) : NfsExtraOptions;
|
|
|
|
|
|
|
|
|
|
var ctx = new RenderContext
|
|
|
|
|
{
|
|
|
|
|
CustomerName = CustomerName.Trim(),
|
|
|
|
|
CustomerAbbrev = abbrev,
|
|
|
|
|
StackName = stackName,
|
|
|
|
|
CmsServerName = cmsServerName,
|
|
|
|
|
HostHttpPort = 80,
|
|
|
|
|
CmsImage = cmsImage,
|
|
|
|
|
MemcachedImage = memcachedImage,
|
|
|
|
|
QuickChartImage = quickChartImage,
|
|
|
|
|
NewtImage = newtImage,
|
|
|
|
|
ThemeHostPath = themePath,
|
|
|
|
|
MySqlHost = mySqlHost,
|
|
|
|
|
MySqlPort = mySqlPort,
|
|
|
|
|
MySqlDatabase = mySqlDbName,
|
|
|
|
|
MySqlUser = mySqlUser,
|
|
|
|
|
SmtpServer = smtpServer,
|
|
|
|
|
SmtpUsername = smtpUsername,
|
|
|
|
|
SmtpPassword = smtpPassword,
|
|
|
|
|
SmtpUseTls = smtpUseTls,
|
|
|
|
|
SmtpUseStartTls = smtpUseStartTls,
|
|
|
|
|
SmtpRewriteDomain = smtpRewriteDomain,
|
|
|
|
|
SmtpHostname = smtpHostname,
|
|
|
|
|
SmtpFromLineOverride = smtpFromLineOverride,
|
|
|
|
|
PhpPostMaxSize = phpPostMaxSize,
|
|
|
|
|
PhpUploadMaxFilesize = phpUploadMaxFilesize,
|
|
|
|
|
PhpMaxExecutionTime = phpMaxExecutionTime,
|
|
|
|
|
PangolinEndpoint = pangolinEndpoint,
|
|
|
|
|
NewtId = string.IsNullOrWhiteSpace(NewtId) ? null : NewtId.Trim(),
|
|
|
|
|
NewtSecret = string.IsNullOrWhiteSpace(NewtSecret) ? null : NewtSecret.Trim(),
|
|
|
|
|
NfsServer = nfsServer,
|
|
|
|
|
NfsExport = nfsExport,
|
|
|
|
|
NfsExportFolder = nfsExportFolder,
|
|
|
|
|
NfsExtraOptions = nfsOptions,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
PreviewYml = composer.Render(templateConfig.Yaml, ctx);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
PreviewYml = $"# Error rendering YML preview:\n# {ex.Message}";
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
IsLoadingYml = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private async Task CopyYmlAsync()
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrEmpty(PreviewYml)) return;
|
|
|
|
|
var mainWindow = (Application.Current?.ApplicationLifetime
|
|
|
|
|
as IClassicDesktopStyleApplicationLifetime)?.MainWindow;
|
|
|
|
|
if (mainWindow is null) return;
|
|
|
|
|
var clipboard = TopLevel.GetTopLevel(mainWindow)?.Clipboard;
|
|
|
|
|
if (clipboard is not null)
|
|
|
|
|
await clipboard.SetTextAsync(PreviewYml);
|
2026-02-18 10:43:27 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private async Task DeployAsync()
|
|
|
|
|
{
|
|
|
|
|
// ── Validation ───────────────────────────────────────────────────
|
|
|
|
|
if (SelectedSshHost == null)
|
|
|
|
|
{
|
|
|
|
|
StatusMessage = "Select an SSH host first.";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (string.IsNullOrWhiteSpace(CustomerName))
|
|
|
|
|
{
|
|
|
|
|
StatusMessage = "Customer Name is required.";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!Valid)
|
|
|
|
|
{
|
|
|
|
|
StatusMessage = "Abbreviation must be exactly 3 lowercase letters (a-z).";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
IsBusy = true;
|
|
|
|
|
StatusMessage = "Starting deployment...";
|
|
|
|
|
DeployOutput = string.Empty;
|
|
|
|
|
ProgressPercent = 0;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
2026-02-19 08:27:54 -05:00
|
|
|
// Wire SSH host into docker services (singletons must know the target host before
|
|
|
|
|
// InstanceService uses them internally for secrets and CLI operations)
|
2026-02-18 10:43:27 -05:00
|
|
|
var dockerCli = _services.GetRequiredService<SshDockerCliService>();
|
|
|
|
|
dockerCli.SetHost(SelectedSshHost);
|
|
|
|
|
var dockerSecrets = _services.GetRequiredService<SshDockerSecretsService>();
|
|
|
|
|
dockerSecrets.SetHost(SelectedSshHost);
|
|
|
|
|
|
|
|
|
|
using var scope = _services.CreateScope();
|
|
|
|
|
var instanceSvc = scope.ServiceProvider.GetRequiredService<InstanceService>();
|
|
|
|
|
|
2026-02-19 08:27:54 -05:00
|
|
|
// InstanceService.CreateInstanceAsync handles the full provisioning flow:
|
|
|
|
|
// 1. Clone template repo
|
|
|
|
|
// 2. Generate MySQL password → create Docker Swarm secret
|
|
|
|
|
// 3. Create MySQL database + SQL user (same password as the secret)
|
|
|
|
|
// 4. Render compose YAML → deploy stack
|
|
|
|
|
SetProgress(30, "Provisioning instance (MySQL user, secrets, stack)...");
|
2026-02-18 10:43:27 -05:00
|
|
|
|
|
|
|
|
var dto = new CreateInstanceDto
|
|
|
|
|
{
|
|
|
|
|
CustomerName = CustomerName.Trim(),
|
|
|
|
|
CustomerAbbrev = Abbrev,
|
|
|
|
|
SshHostId = SelectedSshHost.Id,
|
|
|
|
|
NewtId = string.IsNullOrWhiteSpace(NewtId) ? null : NewtId.Trim(),
|
|
|
|
|
NewtSecret = string.IsNullOrWhiteSpace(NewtSecret) ? null : NewtSecret.Trim(),
|
2026-02-25 17:39:17 -05:00
|
|
|
NfsServer = string.IsNullOrWhiteSpace(NfsServer) ? null : NfsServer.Trim(),
|
|
|
|
|
NfsExport = string.IsNullOrWhiteSpace(NfsExport) ? null : NfsExport.Trim(),
|
|
|
|
|
NfsExportFolder = string.IsNullOrWhiteSpace(NfsExportFolder) ? null : NfsExportFolder.Trim(),
|
|
|
|
|
NfsExtraOptions = string.IsNullOrWhiteSpace(NfsExtraOptions) ? null : NfsExtraOptions.Trim(),
|
|
|
|
|
PurgeStaleVolumes = PurgeStaleVolumes,
|
2026-02-18 10:43:27 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var result = await instanceSvc.CreateInstanceAsync(dto);
|
|
|
|
|
|
|
|
|
|
AppendOutput(result.Output ?? string.Empty);
|
|
|
|
|
|
2026-02-25 17:39:17 -05:00
|
|
|
if (result.Success)
|
|
|
|
|
{
|
|
|
|
|
SetProgress(100, "Stack deployed successfully.");
|
|
|
|
|
StatusMessage = $"Instance '{Abbrev}-cms-stack' deployed in {result.DurationMs}ms. " +
|
|
|
|
|
"Open the details pane on the Instances page to complete setup.";
|
|
|
|
|
_mainVm.NavigateToInstancesWithSelection(Abbrev);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
SetProgress(0, "Deployment failed.");
|
|
|
|
|
StatusMessage = $"Deploy failed: {result.ErrorMessage}";
|
|
|
|
|
}
|
2026-02-18 10:43:27 -05:00
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
StatusMessage = $"Error: {ex.Message}";
|
|
|
|
|
AppendOutput(ex.ToString());
|
|
|
|
|
SetProgress(0, "Failed.");
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
IsBusy = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void SetProgress(double pct, string step)
|
|
|
|
|
{
|
|
|
|
|
ProgressPercent = pct;
|
|
|
|
|
ProgressStep = step;
|
|
|
|
|
AppendOutput($"[{pct:0}%] {step}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void AppendOutput(string text)
|
|
|
|
|
{
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(text))
|
|
|
|
|
DeployOutput += (DeployOutput.Length > 0 ? "\n" : "") + text;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|