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:
Hermes
2026-08-22 23:50:34 -07:00
commit 86e4c9fc81
29 changed files with 4600 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
using System.Net.Http.Json;
using System.Text.Json;
using DashCaddy.Desktop.Models;
namespace DashCaddy.Desktop.Services;
public class ApiClient
{
private readonly HttpClient _httpClient;
private readonly string _baseUrl;
public ApiClient(string baseUrl)
{
_baseUrl = baseUrl.TrimEnd('/');
_httpClient = new HttpClient
{
BaseAddress = new Uri(_baseUrl),
Timeout = TimeSpan.FromSeconds(30)
};
}
public async Task<List<ServiceModel>> GetServicesAsync()
{
var response = await _httpClient.GetAsync("/api/v1/services");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ApiResponse<List<ServiceModel>>>();
return result?.Data ?? new List<ServiceModel>();
}
public async Task<ServiceModel> GetServiceAsync(string id)
{
var response = await _httpClient.GetAsync($"/api/v1/services/{id}");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ApiResponse<ServiceModel>>();
return result?.Data;
}
public async Task<ServiceModel> CreateServiceAsync(ServiceModel service)
{
var response = await _httpClient.PostAsJsonAsync("/api/v1/services", service);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ApiResponse<ServiceModel>>();
return result?.Data;
}
public async Task<ServiceModel> UpdateServiceAsync(string id, ServiceModel service)
{
var response = await _httpClient.PutAsJsonAsync($"/api/v1/services/{id}", service);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ApiResponse<ServiceModel>>();
return result?.Data;
}
public async Task RemoveServiceAsync(string id)
{
var response = await _httpClient.DeleteAsync($"/api/v1/services/{id}");
response.EnsureSuccessStatusCode();
}
public async Task<HealthResponse> GetHealthAsync()
{
var response = await _httpClient.GetAsync("/api/v1/health");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ApiResponse<HealthResponse>>();
return result?.Data ?? new HealthResponse();
}
public async Task<Dictionary<string, string>> GetConfigAsync()
{
var response = await _httpClient.GetAsync("/api/v1/config");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ApiResponse<Dictionary<string, string>>>();
return result?.Data ?? new Dictionary<string, string>();
}
public async Task UpdateConfigAsync(Dictionary<string, string> config)
{
var response = await _httpClient.PutAsJsonAsync("/api/v1/config", config);
response.EnsureSuccessStatusCode();
}
private class ApiResponse<T>
{
public bool Success { get; set; }
public T Data { get; set; }
public string Error { get; set; }
}
}
+57
View File
@@ -0,0 +1,57 @@
using System.Text;
using DashCaddy.Desktop.Models;
namespace DashCaddy.Desktop.Services;
public class CaddyConfigGenerator
{
private const string CaddyfilePath = @"E:\dockerdata\dashcaddy\caddy\Caddyfile";
public void RegenerateConfig(List<ServiceModel> services)
{
var sb = new StringBuilder();
// Global options
sb.AppendLine("{");
sb.AppendLine(" admin :2019");
sb.AppendLine(" email admin@example.com");
sb.AppendLine("}");
sb.AppendLine();
// Each service gets a site block
foreach (var svc in services.Where(s => s.Health != "unhealthy"))
{
var domain = ExtractDomain(svc.Url);
if (string.IsNullOrEmpty(domain)) continue;
sb.AppendLine($"{domain} {{");
sb.AppendLine($" reverse_proxy {svc.Host}:{svc.Port}");
sb.AppendLine(" tls internal");
sb.AppendLine("}");
sb.AppendLine();
}
File.WriteAllText(CaddyfilePath, sb.ToString());
}
public async Task ReloadCaddyAsync()
{
// Call Caddy admin API to reload config
using var http = new HttpClient();
await http.PostAsync("http://localhost:2019/load",
new StringContent(File.ReadAllText(CaddyfilePath), Encoding.UTF8, "text/caddyfile"));
}
private string ExtractDomain(string url)
{
try
{
var uri = new Uri(url);
return uri.Host;
}
catch
{
return null;
}
}
}
+382
View File
@@ -0,0 +1,382 @@
using System.Net.Http.Json;
using DashCaddy.Desktop.Models;
namespace DashCaddy.Desktop.Services;
public enum DnsProviderType
{
None, // No DNS management - use existing DNS
Technitium,
CoreDNS,
Cloudflare,
Route53,
Custom
}
public class DnsProviderConfig
{
public DnsProviderType Type { get; set; }
public string Name { get; set; }
public string BaseUrl { get; set; }
public string ApiKey { get; set; }
public string ZoneId { get; set; }
public Dictionary<string, string> Extra { get; set; } = new();
}
public class DnsClient
{
private readonly HttpClient _httpClient;
private readonly DnsProviderConfig _config;
public DnsProviderType ProviderType => _config?.Type ?? DnsProviderType.None;
public DnsClient(DnsProviderConfig config)
{
_config = config;
if (config?.Type != DnsProviderType.None)
{
_httpClient = new HttpClient { BaseAddress = new Uri(config.BaseUrl) };
if (!string.IsNullOrEmpty(config.ApiKey))
{
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {config.ApiKey}");
}
}
}
public static DnsClient Create(DnsProviderType type, Dictionary<string, string> settings)
{
if (type == DnsProviderType.None)
{
return null;
}
var config = type switch
{
DnsProviderType.Technitium => new DnsProviderConfig
{
Type = DnsProviderType.Technitium,
Name = "Technitium DNS",
BaseUrl = settings.GetValueOrDefault("technitium_url", "http://localhost:5380/api"),
ApiKey = settings.GetValueOrDefault("technitium_api_key", ""),
ZoneId = settings.GetValueOrDefault("technitium_zone", "")
},
DnsProviderType.CoreDNS => new DnsProviderConfig
{
Type = DnsProviderType.CoreDNS,
Name = "CoreDNS",
BaseUrl = settings.GetValueOrDefault("coredns_api_url", "http://localhost:8080"),
ApiKey = "",
ZoneId = settings.GetValueOrDefault("coredns_zone", "local")
},
DnsProviderType.Cloudflare => new DnsProviderConfig
{
Type = DnsProviderType.Cloudflare,
Name = "Cloudflare",
BaseUrl = "https://api.cloudflare.com/client/v4",
ApiKey = settings.GetValueOrDefault("cloudflare_api_token", ""),
ZoneId = settings.GetValueOrDefault("cloudflare_zone_id", "")
},
DnsProviderType.Route53 => new DnsProviderConfig
{
Type = DnsProviderType.Route53,
Name = "AWS Route 53",
BaseUrl = "https://route53.amazonaws.com",
ApiKey = settings.GetValueOrDefault("aws_access_key", ""),
Extra = new Dictionary<string, string>
{
["secret_key"] = settings.GetValueOrDefault("aws_secret_key", ""),
["region"] = settings.GetValueOrDefault("aws_region", "us-east-1")
}
},
_ => new DnsProviderConfig
{
Type = DnsProviderType.Custom,
Name = "Custom DNS",
BaseUrl = settings.GetValueOrDefault("custom_dns_url", ""),
ApiKey = settings.GetValueOrDefault("custom_dns_key", "")
}
};
return new DnsClient(config);
}
public async Task<List<DnsRecord>> GetRecordsAsync(string zone = null)
{
if (_config?.Type == DnsProviderType.None) return new List<DnsRecord>();
zone ??= _config?.ZoneId;
if (string.IsNullOrEmpty(zone)) return new List<DnsRecord>();
return _config.Type switch
{
DnsProviderType.Technitium => await GetTechnitiumRecordsAsync(zone),
DnsProviderType.CoreDNS => await GetCoreDnsRecordsAsync(zone),
DnsProviderType.Cloudflare => await GetCloudflareRecordsAsync(zone),
DnsProviderType.Route53 => await GetRoute53RecordsAsync(zone),
_ => new List<DnsRecord>()
};
}
public async Task<DnsRecord> CreateRecordAsync(string zone, DnsRecord record)
{
if (_config?.Type == DnsProviderType.None) return record;
return _config.Type switch
{
DnsProviderType.Technitium => await CreateTechnitiumRecordAsync(zone, record),
DnsProviderType.CoreDNS => await CreateCoreDnsRecordAsync(zone, record),
DnsProviderType.Cloudflare => await CreateCloudflareRecordAsync(zone, record),
DnsProviderType.Route53 => await CreateRoute53RecordAsync(zone, record),
_ => record
};
}
public async Task<DnsRecord> UpdateRecordAsync(string zone, string recordId, DnsRecord record)
{
if (_config?.Type == DnsProviderType.None) return record;
return _config.Type switch
{
DnsProviderType.Technitium => await UpdateTechnitiumRecordAsync(zone, recordId, record),
DnsProviderType.CoreDNS => await UpdateCoreDnsRecordAsync(zone, recordId, record),
DnsProviderType.Cloudflare => await UpdateCloudflareRecordAsync(zone, recordId, record),
DnsProviderType.Route53 => await UpdateRoute53RecordAsync(zone, recordId, record),
_ => record
};
}
public async Task DeleteRecordAsync(string zone, string recordId)
{
if (_config?.Type == DnsProviderType.None) return;
switch (_config.Type)
{
case DnsProviderType.Technitium:
await DeleteTechnitiumRecordAsync(zone, recordId);
break;
case DnsProviderType.CoreDNS:
await DeleteCoreDnsRecordAsync(zone, recordId);
break;
case DnsProviderType.Cloudflare:
await DeleteCloudflareRecordAsync(zone, recordId);
break;
case DnsProviderType.Route53:
await DeleteRoute53RecordAsync(zone, recordId);
break;
}
}
// ─── Technitium ───
private async Task<List<DnsRecord>> GetTechnitiumRecordsAsync(string zone)
{
var response = await _httpClient.GetAsync($"/zones/{zone}/records");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<TechnitiumZoneResponse>();
return result?.Records?.Select(r => new DnsRecord
{
Id = r.Id,
Name = r.Name,
Type = r.Type,
Content = r.Value,
Ttl = r.Ttl
}).ToList() ?? new List<DnsRecord>();
}
private async Task<DnsRecord> CreateTechnitiumRecordAsync(string zone, DnsRecord record)
{
var req = new TechnitiumRecordRequest
{
Name = record.Name,
Type = record.Type,
Value = record.Content,
Ttl = record.Ttl
};
var response = await _httpClient.PostAsJsonAsync($"/zones/{zone}/records", req);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<TechnitiumRecordResponse>();
record.Id = result?.Record?.Id;
return record;
}
private async Task<DnsRecord> UpdateTechnitiumRecordAsync(string zone, string recordId, DnsRecord record)
{
var req = new TechnitiumRecordRequest
{
Name = record.Name,
Type = record.Type,
Value = record.Content,
Ttl = record.Ttl
};
var response = await _httpClient.PutAsJsonAsync($"/zones/{zone}/records/{recordId}", req);
response.EnsureSuccessStatusCode();
return record;
}
private async Task DeleteTechnitiumRecordAsync(string zone, string recordId)
{
var response = await _httpClient.DeleteAsync($"/zones/{zone}/records/{recordId}");
response.EnsureSuccessStatusCode();
}
// ─── CoreDNS (via API wrapper) ───
private async Task<List<DnsRecord>> GetCoreDnsRecordsAsync(string zone)
{
var response = await _httpClient.GetAsync($"/api/v1/dns/zones/{zone}/records");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ApiResponse<List<DnsRecord>>>();
return result?.Data ?? new List<DnsRecord>();
}
private async Task<DnsRecord> CreateCoreDnsRecordAsync(string zone, DnsRecord record)
{
var response = await _httpClient.PostAsJsonAsync($"/api/v1/dns/zones/{zone}/records", record);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ApiResponse<DnsRecord>>();
return result?.Data ?? record;
}
private async Task<DnsRecord> UpdateCoreDnsRecordAsync(string zone, string recordId, DnsRecord record)
{
var response = await _httpClient.PutAsJsonAsync($"/api/v1/dns/zones/{zone}/records/{recordId}", record);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ApiResponse<DnsRecord>>();
return result?.Data ?? record;
}
private async Task DeleteCoreDnsRecordAsync(string zone, string recordId)
{
var response = await _httpClient.DeleteAsync($"/api/v1/dns/zones/{zone}/records/{recordId}");
response.EnsureSuccessStatusCode();
}
// ─── Cloudflare ───
private async Task<List<DnsRecord>> GetCloudflareRecordsAsync(string zone)
{
var response = await _httpClient.GetAsync($"/zones/{zone}/dns_records");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<CloudflareResponse>();
return result?.Result?.Select(r => new DnsRecord
{
Id = r.Id,
Name = r.Name,
Type = r.Type,
Content = r.Content,
Ttl = r.Ttl ?? 300,
Proxied = r.Proxied ?? false
}).ToList() ?? new List<DnsRecord>();
}
private async Task<DnsRecord> CreateCloudflareRecordAsync(string zone, DnsRecord record)
{
var req = new CloudflareRecordRequest
{
Type = record.Type,
Name = record.Name,
Content = record.Content,
Ttl = record.Ttl,
Proxied = record.Proxied
};
var response = await _httpClient.PostAsJsonAsync($"/zones/{zone}/dns_records", req);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<CloudflareSingleResponse>();
record.Id = result?.Result?.Id;
return record;
}
private async Task<DnsRecord> UpdateCloudflareRecordAsync(string zone, string recordId, DnsRecord record)
{
var req = new CloudflareRecordRequest
{
Type = record.Type,
Name = record.Name,
Content = record.Content,
Ttl = record.Ttl,
Proxied = record.Proxied
};
var response = await _httpClient.PutAsJsonAsync($"/zones/{zone}/dns_records/{recordId}", req);
response.EnsureSuccessStatusCode();
return record;
}
private async Task DeleteCloudflareRecordAsync(string zone, string recordId)
{
var response = await _httpClient.DeleteAsync($"/zones/{zone}/dns_records/{recordId}");
response.EnsureSuccessStatusCode();
}
// ─── Route 53 ───
private async Task<List<DnsRecord>> GetRoute53RecordsAsync(string zone)
{
return new List<DnsRecord>();
}
private async Task<DnsRecord> CreateRoute53RecordAsync(string zone, DnsRecord record) => record;
private async Task<DnsRecord> UpdateRoute53RecordAsync(string zone, string recordId, DnsRecord record) => record;
private async Task DeleteRoute53RecordAsync(string zone, string recordId) { }
// ─── Response DTOs ───
private class ApiResponse<T>
{
public bool Success { get; set; }
public T Data { get; set; }
public string Error { get; set; }
}
private class TechnitiumZoneResponse
{
public List<TechnitiumRecord> Records { get; set; }
}
private class TechnitiumRecord
{
public string Id { get; set; }
public string Name { get; set; }
public string Type { get; set; }
public string Value { get; set; }
public int Ttl { get; set; }
}
private class TechnitiumRecordRequest
{
public string Name { get; set; }
public string Type { get; set; }
public string Value { get; set; }
public int Ttl { get; set; }
}
private class TechnitiumRecordResponse
{
public TechnitiumRecord Record { get; set; }
}
private class CloudflareResponse
{
public bool Success { get; set; }
public List<CloudflareRecord> Result { get; set; }
}
private class CloudflareRecord
{
public string Id { get; set; }
public string Name { get; set; }
public string Type { get; set; }
public string Content { get; set; }
public int? Ttl { get; set; }
public bool? Proxied { get; set; }
}
private class CloudflareRecordRequest
{
public string Type { get; set; }
public string Name { get; set; }
public string Content { get; set; }
public int Ttl { get; set; }
public bool Proxied { get; set; }
}
private class CloudflareSingleResponse
{
public bool Success { get; set; }
public CloudflareRecord Result { get; set; }
}
}
+125
View File
@@ -0,0 +1,125 @@
using Docker.DotNet;
using Docker.DotNet.Models;
using DashCaddy.Desktop.Models;
using System.Text.Json;
namespace DashCaddy.Desktop.Services;
public class DockerService
{
private readonly DockerClient _client;
public DockerService(string endpoint)
{
_client = new DockerClientConfiguration(new Uri(endpoint)).CreateClient();
}
public async Task<List<ContainerListResponse>> GetContainersAsync(string labelFilter = null)
{
var parameters = new ContainersListParameters
{
All = true,
Filters = new Dictionary<string, IDictionary<string, bool>>
{
["label"] = new Dictionary<string, bool> { ["dashcaddy.managed"] = true }
}
};
if (!string.IsNullOrEmpty(labelFilter))
{
parameters.Filters["name"] = new Dictionary<string, bool> { [labelFilter] = true };
}
return await _client.Containers.ListContainersAsync(parameters);
}
public async Task<ContainerInspectResponse> GetContainerAsync(string id)
{
return await _client.Containers.InspectContainerAsync(id);
}
public async Task StartContainerAsync(string id)
{
await _client.Containers.StartContainerAsync(id, null);
}
public async Task StopContainerAsync(string id)
{
await _client.Containers.StopContainerAsync(id, new ContainerStopParameters { WaitBeforeKillSeconds = 10 });
}
public async Task RestartContainerAsync(string id)
{
await _client.Containers.RestartContainerAsync(id, new ContainerRestartParameters { WaitBeforeKillSeconds = 10 });
}
public async Task RemoveContainerAsync(string id)
{
await _client.Containers.RemoveContainerAsync(id, new ContainerRemoveParameters { Force = true, RemoveVolumes = false });
}
public async Task CreateContainerAsync(ServiceModel service)
{
var createParams = new CreateContainerParameters
{
Image = service.Image,
Name = $"dashcaddy-{service.Id}",
Labels = new Dictionary<string, string>
{
["dashcaddy.managed"] = "true",
["dashcaddy.service-id"] = service.Id,
["dashcaddy.service-name"] = service.Name
},
Env = service.Environment.Select(kvp => $"{kvp.Key}={kvp.Value}").ToList(),
HostConfig = new HostConfig
{
PortBindings = service.Environment.TryGetValue("PORT", out var port) && int.TryParse(port, out var p)
? new Dictionary<string, IList<PortBinding>>
{
[$"{p}/tcp"] = new List<PortBinding> { new PortBinding { HostPort = p.ToString() } }
}
: new Dictionary<string, IList<PortBinding>>(),
RestartPolicy = new RestartPolicy { Name = "unless-stopped" }
}
};
if (service.Volumes != null)
{
createParams.HostConfig.Binds = service.Volumes.ToList();
}
await _client.Containers.CreateContainerAsync(createParams);
}
public async Task<string> GetContainerLogsAsync(string id, int tailLines = 100)
{
var parameters = new ContainerLogsParameters
{
ShowStdout = true,
ShowStderr = true,
Tail = tailLines.ToString()
};
using var stream = await _client.Containers.GetContainerLogsAsync(id, false, parameters);
using var reader = new StreamReader(stream);
return await reader.ReadToEndAsync();
}
public async Task<bool> IsDockerRunningAsync()
{
try
{
await _client.System.PingAsync();
return true;
}
catch
{
return false;
}
}
public async Task<VersionResponse> GetVersionAsync()
{
return await _client.System.GetVersionAsync();
}
}
+284
View File
@@ -0,0 +1,284 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using DashCaddy.Desktop.Models;
namespace DashCaddy.Desktop.Services;
public static class ComposeParser
{
public static List<ServiceModel> Parse(string yamlContent)
{
var services = new List<ServiceModel>();
// Simple YAML parsing - in production use YamlDotNet
// For now, we'll do a basic JSON-based approach assuming compose is converted
try
{
var json = JsonSerializer.Deserialize<JsonNode>(yamlContent); // This won't work for YAML directly
// Real implementation would use YamlDotNet
}
catch
{
// Fallback
}
return services;
}
public static List<ServiceModel> ParseJson(string jsonContent)
{
var compose = JsonSerializer.Deserialize<ComposeFile>(jsonContent, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
var services = new List<ServiceModel>();
if (compose?.Services != null)
{
foreach (var (name, svc) in compose.Services)
{
var port = ExtractPort(svc);
var env = svc.Environment ?? new Dictionary<string, string>();
services.Add(new ServiceModel
{
Id = Guid.NewGuid().ToString("N")[..8],
Name = name,
Type = "docker-compose",
Image = svc.Image ?? "",
Host = "localhost",
Port = port,
Environment = env,
Volumes = svc.Volumes ?? new List<string>(),
Labels = svc.Labels ?? new Dictionary<string, string>(),
ComposeFile = jsonContent
});
}
}
return services;
}
private static int ExtractPort(ComposeService svc)
{
if (svc.Ports != null)
{
foreach (var (key, value) in svc.Ports)
{
if (int.TryParse(key.Split(':')[0], out var hostPort))
return hostPort;
if (int.TryParse(key, out var containerPort))
return containerPort;
}
}
return 80;
}
}
public static class TemplateRegistry
{
private static readonly Dictionary<string, ServiceTemplate> Templates = new()
{
["plex"] = new ServiceTemplate
{
Id = "plex",
Name = "Plex Media Server",
Description = "Media server for movies, TV shows, and music",
Icon = "📺",
DefaultPort = 32400,
Image = "plexinc/pms-docker:latest",
Environment = new Dictionary<string, string>
{
["PLEX_CLAIM"] = "",
["TZ"] = "America/Los_Angeles"
},
Volumes = new List<string>
{
"E:/dockerdata/plex/config:/config",
"E:/dockerdata/plex/transcode:/transcode",
"E:/media:/data"
},
RequiredVariables = new[] { "PLEX_CLAIM" }
},
["homeassistant"] = new ServiceTemplate
{
Id = "homeassistant",
Name = "Home Assistant",
Description = "Open source home automation platform",
Icon = "🏠",
DefaultPort = 8123,
Image = "ghcr.io/home-assistant/home-assistant:stable",
Environment = new Dictionary<string, string>
{
["TZ"] = "America/Los_Angeles"
},
Volumes = new List<string>
{
"E:/dockerdata/homeassistant:/config"
},
NetworkMode = "host"
},
["jellyfin"] = new ServiceTemplate
{
Id = "jellyfin",
Name = "Jellyfin",
Description = "Free software media system",
Icon = "🎬",
DefaultPort = 8096,
Image = "jellyfin/jellyfin:latest",
Environment = new Dictionary<string, string>
{
["TZ"] = "America/Los_Angeles"
},
Volumes = new List<string>
{
"E:/dockerdata/jellyfin/config:/config",
"E:/dockerdata/jellyfin/cache:/cache",
"E:/media:/media"
}
},
["portainer"] = new ServiceTemplate
{
Id = "portainer",
Name = "Portainer",
Description = "Docker container management UI",
Icon = "🐳",
DefaultPort = 9443,
Image = "portainer/portainer-ce:latest",
Volumes = new List<string>
{
"E:/dockerdata/portainer:/data",
"//./pipe/docker_engine:/var/run/docker.sock"
}
},
["adguard"] = new ServiceTemplate
{
Id = "adguard",
Name = "AdGuard Home",
Description = "Network-wide ad blocking & DNS",
Icon = "🛡️",
DefaultPort = 3000,
Image = "adguard/adguardhome:latest",
Volumes = new List<string>
{
"E:/dockerdata/adguard/work:/opt/adguardhome/work",
"E:/dockerdata/adguard/conf:/opt/adguardhome/conf"
},
Ports = new Dictionary<int, int> { [53] = 53, [3000] = 3000 }
},
["uptime-kuma"] = new ServiceTemplate
{
Id = "uptime-kuma",
Name = "Uptime Kuma",
Description = "Self-hosted monitoring tool",
Icon = "📊",
DefaultPort = 3001,
Image = "louislam/uptime-kuma:1",
Volumes = new List<string>
{
"E:/dockerdata/uptime-kuma:/app/data"
}
},
["vaultwarden"] = new ServiceTemplate
{
Id = "vaultwarden",
Name = "Vaultwarden (Bitwarden)",
Description = "Self-hosted password manager",
Icon = "🔐",
DefaultPort = 8080,
Image = "vaultwarden/server:latest",
Environment = new Dictionary<string, string>
{
["SIGNUPS_ALLOWED"] = "true",
["INVITATIONS_ALLOWED"] = "true"
},
Volumes = new List<string>
{
"E:/dockerdata/vaultwarden:/data"
}
},
["paperless"] = new ServiceTemplate
{
Id = "paperless",
Name = "Paperless-ngx",
Description = "Document management system",
Icon = "📄",
DefaultPort = 8000,
Image = "ghcr.io/paperless-ngx/paperless-ngx:latest",
Environment = new Dictionary<string, string>
{
["PAPERLESS_REDIS"] = "redis://localhost:6379",
["PAPERLESS_DBHOST"] = "localhost",
["PAPERLESS_DBNAME"] = "paperless",
["PAPERLESS_DBUSER"] = "paperless",
["PAPERLESS_DBPASS"] = "paperless"
},
Volumes = new List<string>
{
"E:/dockerdata/paperless/data:/usr/src/paperless/data",
"E:/dockerdata/paperless/media:/usr/src/paperless/media",
"E:/dockerdata/paperless/export:/usr/src/paperless/export",
"E:/dockerdata/paperless/consume:/usr/src/paperless/consume"
}
}
};
public static ServiceTemplate Get(string id)
{
return Templates.TryGetValue(id, out var template) ? template : null;
}
public static IEnumerable<ServiceTemplate> GetAll()
{
return Templates.Values;
}
}
public class ServiceTemplate
{
public string Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Icon { get; set; }
public int DefaultPort { get; set; }
public string Image { get; set; }
public Dictionary<string, string> Environment { get; set; } = new();
public List<string> Volumes { get; set; } = new();
public Dictionary<int, int> Ports { get; set; } = new();
public string NetworkMode { get; set; }
public string[] RequiredVariables { get; set; } = Array.Empty<string>();
public ServiceModel Instantiate(Dictionary<string, string> variables)
{
var env = new Dictionary<string, string>(Environment);
foreach (var (key, value) in variables)
{
env[key] = value;
}
var port = DefaultPort;
if (Ports.Count > 0)
{
port = Ports.Keys.First();
}
return new ServiceModel
{
Id = Guid.NewGuid().ToString("N")[..8],
Name = Name,
Type = "template",
Image = Image,
Host = "localhost",
Port = port,
Environment = env,
Volumes = Volumes,
Labels = new Dictionary<string, string>
{
["dashcaddy.template"] = Id,
["dashcaddy.managed"] = "true"
}
};
}
}