wip: Windows desktop app scaffold (WinUI 3/.NET 8) + NSIS installer + docs
Owner decision pending (STATE.md DC-100 tick): adopt/ship/park. Preserved from fragile git stash to named branch 2026-08-23. NOTE: requires a Windows build machine (WinUI XAML compiler + MSIX do not cross-build on Linux) — see WINDOWS_APP_BUILD.md in this tree. Secret-scanned clean 2026-08-23 (no keys/tokens/PEM in tree).
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using Microsoft.UI;
|
||||
using DashCaddy.Desktop.ViewModels;
|
||||
using Windows.UI;
|
||||
|
||||
namespace DashCaddy.Desktop.ViewModels;
|
||||
|
||||
public class StatusToColorConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is ServiceHealth health)
|
||||
{
|
||||
return health switch
|
||||
{
|
||||
ServiceHealth.Healthy => new SolidColorBrush(Colors.LimeGreen),
|
||||
ServiceHealth.Degraded => new SolidColorBrush(Colors.Gold),
|
||||
ServiceHealth.Unhealthy => new SolidColorBrush(Colors.Red),
|
||||
_ => new SolidColorBrush(Colors.Gray)
|
||||
};
|
||||
}
|
||||
if (value is bool boolVal)
|
||||
{
|
||||
return boolVal ? new SolidColorBrush(Colors.LimeGreen) : new SolidColorBrush(Colors.Red);
|
||||
}
|
||||
return new SolidColorBrush(Colors.Gray);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public class StatusToTextConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is ServiceHealth health)
|
||||
{
|
||||
return health switch
|
||||
{
|
||||
ServiceHealth.Healthy => "Healthy",
|
||||
ServiceHealth.Degraded => "Degraded",
|
||||
ServiceHealth.Unhealthy => "Unhealthy",
|
||||
_ => "Unknown"
|
||||
};
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public class BoolToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is bool boolVal)
|
||||
{
|
||||
return boolVal ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
return Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public class InverseBoolConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is bool boolVal)
|
||||
return !boolVal;
|
||||
return true;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using DashCaddy.Desktop.Models;
|
||||
using DashCaddy.Desktop.Services;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DashCaddy.Desktop.ViewModels;
|
||||
|
||||
public partial class MainViewModel : ObservableObject
|
||||
{
|
||||
private readonly DockerService _dockerService;
|
||||
private readonly ApiClient _apiClient;
|
||||
private readonly CaddyConfigGenerator _caddyGenerator;
|
||||
private readonly DnsClient _dnsClient;
|
||||
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<ServiceViewModel> _services = new();
|
||||
|
||||
[ObservableProperty]
|
||||
private int _servicesHealthy;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _servicesTotal;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _dnsHealthy = true;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _certsHealthy = true;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _certsDaysRemaining = 45;
|
||||
|
||||
// For dialogs
|
||||
public ObservableCollection<ServiceTemplate> Templates { get; } = new(TemplateRegistry.GetAll());
|
||||
public List<VariableViewModel> TemplateVariables { get; set; } = new();
|
||||
public Dictionary<string, string> TemplateVariablesDict { get; set; } = new();
|
||||
public ServiceTemplate SelectedTemplate { get; set; }
|
||||
public string SelectedComposeFile { get; set; }
|
||||
public object CustomService { get; set; }
|
||||
|
||||
public string ServicesSummary => $"{ServicesHealthy}/{ServicesTotal} running";
|
||||
public string CertsSummary => $"{CertsDaysRemaining} days";
|
||||
public string DashboardUrl => "https://status.local";
|
||||
|
||||
public MainViewModel(DockerService dockerService, ApiClient apiClient, CaddyConfigGenerator caddyGenerator, DnsClient dnsClient)
|
||||
{
|
||||
_dockerService = dockerService;
|
||||
_apiClient = apiClient;
|
||||
_caddyGenerator = caddyGenerator;
|
||||
_dnsClient = dnsClient;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await RefreshServicesAsync();
|
||||
await CheckHealthAsync();
|
||||
}
|
||||
|
||||
public async Task RefreshServicesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var services = await _apiClient.GetServicesAsync();
|
||||
Services.Clear();
|
||||
foreach (var svc in services)
|
||||
{
|
||||
Services.Add(new ServiceViewModel
|
||||
{
|
||||
Id = svc.Id,
|
||||
Name = svc.Name,
|
||||
Type = svc.Type,
|
||||
Url = svc.Url,
|
||||
Health = Enum.TryParse<ServiceHealth>(svc.Health, true, out var h) ? h : ServiceHealth.Unknown,
|
||||
IsRunning = svc.State == "running",
|
||||
Port = svc.Port,
|
||||
Host = svc.Host
|
||||
});
|
||||
}
|
||||
ServicesHealthy = Services.Count(s => s.Health == ServiceHealth.Healthy);
|
||||
ServicesTotal = Services.Count;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await RefreshFromDockerAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshFromDockerAsync()
|
||||
{
|
||||
var containers = await _dockerService.GetContainersAsync("dashcaddy");
|
||||
Services.Clear();
|
||||
foreach (var c in containers)
|
||||
{
|
||||
Services.Add(new ServiceViewModel
|
||||
{
|
||||
Id = c.ID[..12],
|
||||
Name = c.Names.FirstOrDefault()?.TrimStart('/') ?? "unknown",
|
||||
Type = "docker",
|
||||
Url = $"http://localhost:{c.Ports.FirstOrDefault()?.PublicPort ?? 0}",
|
||||
Health = c.State == "running" ? ServiceHealth.Healthy : ServiceHealth.Unhealthy,
|
||||
IsRunning = c.State == "running"
|
||||
});
|
||||
}
|
||||
ServicesHealthy = Services.Count(s => s.Health == ServiceHealth.Healthy);
|
||||
ServicesTotal = Services.Count;
|
||||
}
|
||||
|
||||
public async Task CheckHealthAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var health = await _apiClient.GetHealthAsync();
|
||||
DnsHealthy = health.Dns == "healthy";
|
||||
CertsHealthy = health.Certs == "healthy";
|
||||
CertsDaysRemaining = health.CertsDaysRemaining;
|
||||
}
|
||||
catch
|
||||
{
|
||||
DnsHealthy = false;
|
||||
CertsHealthy = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StartServiceAsync(string id) => await _dockerService.StartContainerAsync(id);
|
||||
public async Task StopServiceAsync(string id) => await _dockerService.StopContainerAsync(id);
|
||||
public async Task RestartServiceAsync(string id) => await _dockerService.RestartContainerAsync(id);
|
||||
|
||||
public async Task RemoveServiceAsync(string id)
|
||||
{
|
||||
await _dockerService.RemoveContainerAsync(id);
|
||||
await _apiClient.RemoveServiceAsync(id);
|
||||
_caddyGenerator.RegenerateConfig(Services.Select(s => s.ToServiceModel()).ToList());
|
||||
await _caddyGenerator.ReloadCaddyAsync();
|
||||
}
|
||||
|
||||
public async Task ImportComposeAsync(string filePath)
|
||||
{
|
||||
var json = await File.ReadAllTextAsync(filePath);
|
||||
var services = ComposeParser.ParseJson(json);
|
||||
|
||||
foreach (var svc in services)
|
||||
{
|
||||
await _apiClient.CreateServiceAsync(svc);
|
||||
await _dockerService.CreateContainerAsync(svc);
|
||||
}
|
||||
|
||||
_caddyGenerator.RegenerateConfig(Services.Select(s => s.ToServiceModel()).ToList());
|
||||
await _caddyGenerator.ReloadCaddyAsync();
|
||||
}
|
||||
|
||||
public async Task<ServiceModel> CreateServiceFromTemplateAsync()
|
||||
{
|
||||
if (SelectedTemplate == null) return null;
|
||||
|
||||
var service = SelectedTemplate.Instantiate(TemplateVariablesDict);
|
||||
|
||||
await _apiClient.CreateServiceAsync(service);
|
||||
await _dockerService.CreateContainerAsync(service);
|
||||
|
||||
_caddyGenerator.RegenerateConfig(Services.Select(s => s.ToServiceModel()).ToList());
|
||||
await _caddyGenerator.ReloadCaddyAsync();
|
||||
|
||||
return service;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace DashCaddy.Desktop.ViewModels;
|
||||
|
||||
public enum ServiceHealth
|
||||
{
|
||||
Healthy,
|
||||
Degraded,
|
||||
Unhealthy,
|
||||
Unknown
|
||||
}
|
||||
|
||||
public partial class ServiceViewModel : ObservableObject
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string Url { get; set; }
|
||||
public int Port { get; set; }
|
||||
public string Host { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
private ServiceHealth _health;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isRunning;
|
||||
|
||||
public ServiceModel ToServiceModel() => new()
|
||||
{
|
||||
Id = Id,
|
||||
Name = Name,
|
||||
Type = Type,
|
||||
Url = Url,
|
||||
Port = Port,
|
||||
Host = Host,
|
||||
Health = Health,
|
||||
State = IsRunning ? "running" : "stopped"
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user