Files
123123/ZA.CoreService.ESBCertificateManager/Form1.cs
GizzlerandCursor 76e280d809 Simplify actions to one deploy button and per-row restart menu.
Keep only the yellow Austauschen + Neustart button; move manual restart into a three-dot row menu and probe TLS status after restart.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 13:21:35 +02:00

2448 lines
82 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using ZA.CoreService.ESBCertificateManager.Configuration;
using ZA.CoreService.ESBCertificateManager.Models;
using ZA.CoreService.ESBCertificateManager.Services;
using ZA.CoreService.ESBCertificateManager.Setup;
using Microsoft.Data.SqlClient;
namespace ZA.CoreService.ESBCertificateManager
{
public partial class Form1 : Form
{
// Große Hintergrundflächen
private readonly Color BackgroundColor = Color.FromArgb(10, 18, 34); // #0A1222
private readonly Color SidebarColor = Color.FromArgb(7, 23, 45); // #07172D
private readonly Color CardColor = Color.FromArgb(16, 38, 65); // #102641
private readonly Color CardHoverColor = Color.FromArgb(23, 55, 94); // #17375E
private readonly Color BorderColor = Color.FromArgb(36, 74, 117); // #244A75
// ZIEHL-ABEGG-nahe Markenakzente
private readonly Color BlueColor = Color.FromArgb(0, 110, 182); // #006EB6
private readonly Color BrightBlueColor = Color.FromArgb(0, 139, 210); // #008BD2
private readonly Color GoldColor = Color.FromArgb(208, 171, 57); // #D0AB39
private readonly Color LightGoldColor = Color.FromArgb(226, 196, 93); // #E2C45D
// Schrift und Statusfarben
private readonly Color TextColor = Color.FromArgb(241, 245, 249); // #F1F5F9
private readonly Color MutedTextColor = Color.FromArgb(169, 184, 200); // #A9B8C8
private readonly Color GreenColor = Color.FromArgb(60, 203, 127); // #3CCB7F
private readonly Color RedColor = Color.FromArgb(239, 106, 106); // #EF6A6A
private readonly AppSettings _settings;
private DeploymentOrchestrator? _orchestrator;
private readonly SonicConnectionRepository _sonicConnectionRepository;
private readonly DeploymentTargetRepository _deploymentTargetRepository;
private readonly SonicSetupRepository _sonicSetupRepository;
private readonly LocalSetupSelectionStore _localSetupSelectionStore;
private IReadOnlyList<DatabaseSonicConnection>
_databaseSonicConnections = [];
private readonly Dictionary<int, SonicCredentialProfile>
_credentialsByConnectionId = new();
private LocalSetupSelection? _localSetupSelection;
private CertificateInfo? _loadedCertificateInfo;
private string? _loadedCertificatePassword;
private IReadOnlyList<DeploymentTarget> _loadedTargets = [];
private bool _isOperationRunning;
private CancellationTokenSource? _runCts;
private Label lblStatus = null!;
private Label lblStatusDot = null!;
private TextBox txtCertificatePath = null!;
private DataGridView dgvTargets = null!;
private ComboBox cmbSonicConnection = null!;
private Label lblSonicStatus = null!;
private Panel navProgressPanel = null!;
private Panel titleBar = null!;
private Button btnMinimize = null!;
private Button btnMaximize = null!;
private Button btnClose = null!;
private Button btnDeploy = null!;
private Button btnCancelRun = null!;
private ContextMenuStrip _targetRowMenu = null!;
private bool isMaximized = false;
private Point dragStartPoint;
private Label lblCertificateSubject = null!;
private Label lblCertificateIssuer = null!;
private Label lblCertificateExpiry = null!;
private Label lblCertificateFingerprint = null!;
private Label lblCertificateValidity = null!;
private Button btnSelectFile = null!;
private bool isCertificateFileLoaded;
private int nvbarlaststate = 1;
public Form1()
{
InitializeComponent();
_settings = AppSettingsLoader.Load();
_sonicConnectionRepository = new SonicConnectionRepository(
_settings.Database.ConnectionString);
_deploymentTargetRepository = new DeploymentTargetRepository(
_settings.Database.ConnectionString);
_sonicSetupRepository = new SonicSetupRepository(
_settings.Database.ConnectionString);
_localSetupSelectionStore = new LocalSetupSelectionStore();
BuildDesign();
Shown += async (_, _) => await InitializeApplicationAsync();
}
private Button CreateWindowButton(string text)
{
return new Button
{
Text = text,
Width = 46,
Height = 34,
FlatStyle = FlatStyle.Flat,
FlatAppearance =
{
BorderSize = 0,
MouseOverBackColor = CardHoverColor
},
BackColor = SidebarColor,
ForeColor = MutedTextColor,
Font = new Font("Segoe UI", 11),
Cursor = Cursors.Hand,
TabStop = false
};
}
private void ToggleMaximizeWindow()
{
if (isMaximized)
{
WindowState = FormWindowState.Normal;
isMaximized = false;
}
else
{
WindowState = FormWindowState.Maximized;
isMaximized = true;
}
}
private void TitleBar_MouseDown(object? sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
dragStartPoint = e.Location;
}
}
private async Task OpenSettingsAsync()
{
if (_isOperationRunning)
{
MessageBox.Show(
this,
"Aktion läuft.",
"Einstellungen",
MessageBoxButtons.OK,
MessageBoxIcon.Information);
return;
}
try
{
SetupCoordinator coordinator = new(
_settings,
new SonicSetupRepository(
_settings.Database.ConnectionString));
using SetupWizardForm settingsForm = new(
coordinator,
isSettingsMode: true);
// Nach dem Schließen immer neu laden:
// Pfade/Container können auch ohne „Profil übernehmen“ angelegt worden sein.
_ = settingsForm.ShowDialog(this);
SetStatus(
"Einstellungen geschlossen. Ziele werden aktualisiert ...",
isError: false);
await InitializeApplicationAsync();
}
catch (Exception ex)
{
MessageBox.Show(
this,
ex.Message,
"Einstellungen",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
}
private async Task InitializeApplicationAsync()
{
SetStatus(
"Java- und Sonic-Runtime werden geprüft ...",
isError: false);
RuntimeEnvironmentValidator runtimeValidator = new();
RuntimeValidationResult runtimeResult =
await runtimeValidator.ValidateAsync(
_settings.Runtime);
if (!runtimeResult.IsValid)
{
string details = string.Join(
Environment.NewLine,
runtimeResult.Issues.Select(
issue => "• " + issue));
throw new InvalidOperationException(
"Die lokale Runtime-Konfiguration ist unvollständig:" +
Environment.NewLine +
Environment.NewLine +
details);
}
try
{
_localSetupSelection =
_localSetupSelectionStore.Load();
_databaseSonicConnections =
await _sonicConnectionRepository.GetActiveAsync(
_settings.EnvironmentCode);
await LoadCredentialsForConnectionsAsync();
List<SonicConnection> runtimeConnections = [];
foreach (DatabaseSonicConnection connection
in _databaseSonicConnections)
{
if (!_credentialsByConnectionId.TryGetValue(
connection.Id,
out SonicCredentialProfile? credential))
{
continue;
}
runtimeConnections.Add(
connection.ToRuntimeConnection(
_settings.Runtime,
credential));
}
_orchestrator = new DeploymentOrchestrator(
_settings,
runtimeConnections);
BindDatabaseSonicConnections();
await LoadTargetsAsync();
if (_databaseSonicConnections.Count == 0)
{
SetStatus(
"SQL-Zugriff erfolgreich. " +
"Noch keine aktiven Sonic-Verbindungen in SQL. " +
"Lokale Testziele wurden geladen.",
isError: false);
return;
}
SetStatus(
$"{_databaseSonicConnections.Count} aktive Sonic-Verbindung(en) " +
$"aus SQL und {_loadedTargets.Count} Bereitstellungsziel(e) geladen.",
isError: false);
}
catch (SqlException ex)
{
_databaseSonicConnections = [];
_credentialsByConnectionId.Clear();
SetStatus(
$"SQL-Zugriff fehlgeschlagen: {ex.Message}",
isError: true);
MessageBox.Show(
this,
ex.Message,
"Fehler",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
catch (InvalidOperationException ex)
{
_databaseSonicConnections = [];
SetStatus(
$"Konfiguration ungültig: {ex.Message}",
isError: true);
MessageBox.Show(
this,
ex.Message,
"Konfiguration",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
}
catch (Exception ex)
{
_databaseSonicConnections = [];
SetStatus(
$"Initialisierung fehlgeschlagen: {ex.Message}",
isError: true);
MessageBox.Show(
this,
ex.Message,
"Fehler",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
}
private void TitleBar_MouseMove(object? sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
Left += e.X - dragStartPoint.X;
Top += e.Y - dragStartPoint.Y;
}
}
private Panel BuildTitleBar()
{
Panel panel = new Panel
{
Height = 34,
BackColor = SidebarColor
};
Label appIcon = new Label
{
Text = "◆",
ForeColor = GoldColor,
Font = new Font("Segoe UI Symbol", 12, FontStyle.Bold),
AutoSize = true,
Location = new Point(12, 8)
};
Label appName = new Label
{
Text = "ESB Certificate Manager",
ForeColor = MutedTextColor,
Font = new Font("Segoe UI", 9),
AutoSize = true,
Location = new Point(31, 9)
};
btnClose = CreateWindowButton("×");
btnMaximize = CreateWindowButton("□");
btnMinimize = CreateWindowButton("");
btnClose.ForeColor = TextColor;
btnClose.FlatAppearance.MouseOverBackColor = Color.FromArgb(190, 35, 45);
btnClose.Click += (_, _) => Close();
btnMinimize.Click += (_, _) =>
{
WindowState = FormWindowState.Minimized;
};
btnMaximize.Click += (_, _) =>
{
ToggleMaximizeWindow();
};
panel.Resize += (_, _) =>
{
btnClose.Left = panel.Width - btnClose.Width;
btnMaximize.Left = btnClose.Left - btnMaximize.Width;
btnMinimize.Left = btnMaximize.Left - btnMinimize.Width;
};
panel.MouseDown += TitleBar_MouseDown;
panel.MouseMove += TitleBar_MouseMove;
appIcon.MouseDown += TitleBar_MouseDown;
appIcon.MouseMove += TitleBar_MouseMove;
appName.MouseDown += TitleBar_MouseDown;
appName.MouseMove += TitleBar_MouseMove;
panel.Controls.Add(appIcon);
panel.Controls.Add(appName);
panel.Controls.Add(btnMinimize);
panel.Controls.Add(btnMaximize);
panel.Controls.Add(btnClose);
return panel;
}
private void BuildDesign()
{
Text = "ESB Certificate Manager";
StartPosition = FormStartPosition.CenterScreen;
MinimumSize = new Size(1100, 700);
Size = new Size(1400, 850);
BackColor = BackgroundColor;
Font = new Font("Segoe UI", 10);
ForeColor = TextColor;
// Entfernt die weiße Windows-Standardtitelleiste.
FormBorderStyle = FormBorderStyle.None;
// Eigene dunkle Titelleiste.
titleBar = BuildTitleBar();
titleBar.Dock = DockStyle.Top;
Controls.Add(titleBar);
Panel sidebar = new Panel
{
Dock = DockStyle.Left,
Width = 205,
BackColor = SidebarColor
};
Panel content = new Panel
{
Dock = DockStyle.Fill,
BackColor = BackgroundColor,
Padding = new Padding(38, 48, 38, 34),
AutoScroll = true
};
// Wichtig für Docking:
// Erst der Fill-Bereich, dann die feste Sidebar.
Controls.Add(content);
Controls.Add(sidebar);
// Dezente vertikale Linie rechts an der Sidebar.
Panel sidebarBorder = new Panel
{
Dock = DockStyle.Right,
Width = 1,
BackColor = BorderColor
};
sidebar.Controls.Add(sidebarBorder);
BuildSidebar(sidebar);
BuildContent(content);
}
private void BuildSidebar(Panel sidebar)
{
// -------------------------
// 1. Logo / Produktbereich
// -------------------------
Panel logoPanel = new Panel
{
Dock = DockStyle.Top,
Height = 165,
Padding = new Padding(18, 18, 18, 8)
};
PictureBox pictureLogo = new PictureBox
{
Location = new Point(18, 14),
Size = new Size(165, 58),
SizeMode = PictureBoxSizeMode.Zoom,
BackColor = Color.Transparent
};
string logoPath = Path.Combine(
Application.StartupPath,
"Assets",
"logo.png");
if (File.Exists(logoPath))
{
pictureLogo.Image = Image.FromFile(logoPath);
}
Label logoSubtitle = new Label
{
Text = "ESB CERTIFICATE MANAGER",
ForeColor = TextColor,
Font = new Font("Segoe UI", 8, FontStyle.Bold),
AutoSize = true,
Location = new Point(18, 88)
};
Panel logoSeparator = new Panel
{
BackColor = BorderColor,
Location = new Point(22, 138),
Size = new Size(168, 1)
};
logoPanel.Controls.Add(pictureLogo);
logoPanel.Controls.Add(logoSubtitle);
logoPanel.Controls.Add(logoSeparator);
// ------------------------------------
// 2. Linker Ablauf: keine Navigation,
// sondern Fortschritt im Deployment
// ------------------------------------
Panel progressPanel = new Panel
{
Dock = DockStyle.Top,
Height = 360,
Padding = new Padding(18, 8, 18, 8)
};
navProgressPanel = progressPanel;
Label progressTitle = new Label
{
Text = "DEPLOYMENT-ABLAUF",
ForeColor = MutedTextColor,
Font = new Font("Segoe UI", 7.5f, FontStyle.Bold),
AutoSize = true,
Location = new Point(18, 12)
};
AddStep(
progressPanel,
y: 48,
number: "1",
title: "Zertifikat",
description: "Datei auswählen",
isActive: true,
hasNextStep: true);
AddStep(
progressPanel,
y: 116,
number: "2",
title: "Ziele",
description: "Systeme auswählen",
isActive: false,
hasNextStep: true);
AddStep(
progressPanel,
y: 184,
number: "3",
title: "Bereitstellen",
description: "Kopieren & Neustart",
isActive: false,
hasNextStep: true);
AddStep(
progressPanel,
y: 252,
number: "4",
title: "TLS-Prüfung",
description: "Zertifikat bestätigen",
isActive: false,
hasNextStep: false);
progressPanel.Controls.Add(progressTitle);
// -----------------------------
// 3. Footer unten: Einstellungen
// -----------------------------
Panel footerPanel = new Panel
{
Dock = DockStyle.Bottom,
Height = 68,
Padding = new Padding(18, 8, 12, 8)
};
Panel footerSeparator = new Panel
{
Dock = DockStyle.Top,
Height = 1,
BackColor = BorderColor
};
Button btnSettings = new Button
{
Text = " Einstellungen",
Location = new Point(14, 14),
Size = new Size(176, 36),
FlatStyle = FlatStyle.Flat,
FlatAppearance =
{
BorderSize = 1,
BorderColor = BorderColor,
MouseOverBackColor = Color.FromArgb(37, 52, 74)
},
BackColor = SidebarColor,
ForeColor = TextColor,
Font = new Font("Segoe UI", 9, FontStyle.Bold),
TextAlign = ContentAlignment.MiddleLeft,
Cursor = Cursors.Hand,
Enabled = !_isOperationRunning
};
btnSettings.Click += async (_, _) =>
await OpenSettingsAsync();
footerPanel.Controls.Add(footerSeparator);
footerPanel.Controls.Add(btnSettings);
// Dock-Reihenfolge: Bottom, Top, Top
sidebar.Controls.Add(footerPanel);
sidebar.Controls.Add(progressPanel);
sidebar.Controls.Add(logoPanel);
}
private void AddStep(
Panel parent,
int y,
string number,
string title,
string description,
bool isActive,
bool hasNextStep)
{
Color stepColor = isActive ? GoldColor : MutedTextColor;
Color titleColor = isActive ? TextColor : MutedTextColor;
Label circle = new Label
{
Name = $"stepCircle{number}",
Text = number,
Location = new Point(18, y),
Size = new Size(25, 25),
TextAlign = ContentAlignment.MiddleCenter,
BackColor = isActive ? GoldColor : Color.FromArgb(25, 48, 76),
ForeColor = isActive ? SidebarColor : MutedTextColor,
Font = new Font("Segoe UI", 8, FontStyle.Bold)
};
Label titleLabel = new Label
{
Name = $"stepTitle{number}",
Text = title,
ForeColor = titleColor,
Font = new Font("Segoe UI", 9.5f, FontStyle.Bold),
AutoSize = true,
Location = new Point(56, y - 1)
};
Label descriptionLabel = new Label
{
Name = $"stepDescription{number}",
Text = description,
ForeColor = MutedTextColor,
Font = new Font("Segoe UI", 8),
AutoSize = true,
Location = new Point(56, y + 16)
};
parent.Controls.Add(circle);
parent.Controls.Add(titleLabel);
parent.Controls.Add(descriptionLabel);
if (hasNextStep)
{
Panel line = new Panel
{
BackColor = BorderColor,
Location = new Point(30, y + 26),
Size = new Size(1, 40)
};
parent.Controls.Add(line);
line.SendToBack();
}
}
private void UpdateToNextStep(int state)
{
Control[] foundCircles = navProgressPanel.Controls.Find($"stepCircle{state}", true);
if (foundCircles.Length == 0) return;
Control[] foundTitels = navProgressPanel.Controls.Find($"stepTitle{state}", true);
if (foundTitels.Length == 0) return;
Label titel = (Label)foundTitels[0];
Label circle = (Label)foundCircles[0];
Control[] foundlastTitels = navProgressPanel.Controls.Find($"stepTitle{nvbarlaststate}", true);
if (foundlastTitels.Length == 0) return;
Label lasttitel = (Label)foundlastTitels[0];
if (nvbarlaststate > state)
{
Control[] foundlastCircles = navProgressPanel.Controls.Find($"stepCircle{nvbarlaststate}", true);
if (foundlastCircles.Length == 0) return;
Label lastcircle = (Label)foundlastCircles[0];
lastcircle.BackColor = Color.FromArgb(25, 48, 76);
lastcircle.ForeColor = MutedTextColor;
lasttitel.ForeColor = MutedTextColor;
}
else
{
lasttitel.ForeColor = MutedTextColor;
}
circle.BackColor = GoldColor;
circle.ForeColor = SidebarColor;
titel.ForeColor = TextColor;
nvbarlaststate = state;
}
private Button CreateNavButton(string text, bool isActive)
{
Button button = new Button
{
Text = text,
Height = 43,
Dock = DockStyle.Top,
FlatStyle = FlatStyle.Flat,
FlatAppearance =
{
BorderSize = 0,
MouseOverBackColor = Color.FromArgb(37, 52, 74)
},
BackColor = isActive ? Color.FromArgb(35, 58, 92) : SidebarColor,
ForeColor = isActive ? TextColor : MutedTextColor,
Font = new Font("Segoe UI", 10, isActive ? FontStyle.Bold : FontStyle.Regular),
TextAlign = ContentAlignment.MiddleLeft,
Padding = new Padding(14, 0, 0, 0),
Cursor = Cursors.Hand
};
button.Click += (_, _) =>
{
SetStatus($"Navigation gewählt: {text.Trim()}", isError: false);
};
return button;
}
private void BuildContent(Panel content)
{
// Status unten zuerst hinzufügen, weil Dock-Reihenfolge relevant ist
Panel statusBar = BuildStatusBar();
statusBar.Dock = DockStyle.Bottom;
content.Controls.Add(statusBar);
Panel actionPanel = BuildActionPanel();
actionPanel.Dock = DockStyle.Bottom;
actionPanel.Height = 65;
content.Controls.Add(actionPanel);
Panel targetsCard = BuildTargetsCard();
targetsCard.Dock = DockStyle.Top;
targetsCard.Height = 380;
targetsCard.Margin = new Padding(0, 18, 0, 0);
content.Controls.Add(targetsCard);
Panel topCards = BuildTopCards();
topCards.Dock = DockStyle.Top;
topCards.Height = 225;
topCards.Margin = new Padding(0, 20, 0, 0);
content.Controls.Add(topCards);
Panel header = BuildHeader();
header.Dock = DockStyle.Top;
header.Height = 87;
content.Controls.Add(header);
}
private Panel BuildHeader()
{
Panel header = new Panel
{
BackColor = BackgroundColor
};
Label title = new Label
{
Text = "Zertifikat bereitstellen",
ForeColor = TextColor,
Font = new Font("Segoe UI", 23, FontStyle.Bold),
AutoSize = true,
Location = new Point(0, 0)
};
Label subtitle = new Label
{
Text = "Verteile ein Zertifikat auf konfigurierte Ziele und prüfe anschließend die TLS-Verbindung.",
ForeColor = MutedTextColor,
Font = new Font("Segoe UI", 10),
AutoSize = true,
Location = new Point(3, 43)
};
header.Controls.Add(title);
header.Controls.Add(subtitle);
return header;
}
private Panel BuildTopCards()
{
Panel container = new Panel
{
BackColor = BackgroundColor
};
Panel sourceCard = CreateCard();
sourceCard.Dock = DockStyle.Left;
sourceCard.Width = 520;
Panel detailsCard = CreateCard();
detailsCard.Dock = DockStyle.Fill;
detailsCard.Margin = new Padding(18, 0, 0, 0);
container.Controls.Add(detailsCard);
container.Controls.Add(sourceCard);
BuildSourceCard(sourceCard);
BuildDetailsCard(detailsCard);
return container;
}
private void BuildSourceCard(Panel card)
{
Label step = CreateSmallTitle("SCHRITT 1");
step.Location = new Point(22, 18);
Label title = CreateCardTitle("Zertifikatsquelle");
title.Location = new Point(22, 40);
Label description = CreateMutedLabel(
"Wähle die Zertifikatsdatei aus der zentralen Ablage.");
description.Location = new Point(22, 73);
txtCertificatePath = new TextBox
{
Location = new Point(22, 112),
Size = new Size(320, 38),
BackColor = Color.FromArgb(20, 30, 48),
ForeColor = TextColor,
BorderStyle = BorderStyle.FixedSingle,
Font = new Font("Segoe UI", 9),
Text = string.Empty,
PlaceholderText = "Keine Datei ausgewählt",
ReadOnly = true
};
btnSelectFile = CreateSecondaryButton("Datei auswählen");
btnSelectFile.Location = new Point(354, 111);
btnSelectFile.Size = new Size(140, 39);
SetFileButtonState(fileLoaded: false);
btnSelectFile.Click += (_, _) =>
{
if (isCertificateFileLoaded)
{
ClearSelectedCertificate();
return;
}
TrySelectCertificateFile();
};
Label hint = new Label
{
Text = "Unterstützte Formate: .cer, .crt, .pem, .pfx, .p12",
ForeColor = MutedTextColor,
Font = new Font("Segoe UI", 8.5f),
AutoSize = true,
Location = new Point(22, 164)
};
card.Controls.Add(step);
card.Controls.Add(title);
card.Controls.Add(description);
card.Controls.Add(txtCertificatePath);
card.Controls.Add(btnSelectFile);
card.Controls.Add(hint);
}
private void TrySelectCertificateFile()
{
using OpenFileDialog dialog = new OpenFileDialog
{
Title = "Zertifikatsdatei auswählen",
Filter =
"Unterstützte Zertifikatsdateien (*.cer;*.crt;*.pem;*.pfx;*.p12)|*.cer;*.crt;*.pem;*.pfx;*.p12|" +
"CER-Zertifikate (*.cer)|*.cer|" +
"CRT-Zertifikate (*.crt)|*.crt|" +
"PEM-Zertifikate (*.pem)|*.pem|" +
"PFX/P12-Zertifikate (*.pfx;*.p12)|*.pfx;*.p12",
FilterIndex = 1,
Multiselect = false,
CheckFileExists = true,
CheckPathExists = true,
RestoreDirectory = true,
AddExtension = true,
DereferenceLinks = true
};
// Bei Abbruch bleibt der bisherige Zustand unverändert.
if (dialog.ShowDialog(this) != DialogResult.OK)
{
return;
}
string selectedPath = dialog.FileName;
if (!IsSupportedCertificateFile(selectedPath))
{
MessageBox.Show(
this,
"Ungültiges Dateiformat.",
"Dateiformat",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
return;
}
CertificateInfo? certificateInfo = TryReadSelectedCertificate(
selectedPath,
out string? certificatePassword);
if (certificateInfo == null)
{
return;
}
txtCertificatePath.Text = selectedPath;
_loadedCertificateInfo = certificateInfo;
_loadedCertificatePassword = certificatePassword;
SetStatus($"Zertifikat ausgewählt: {Path.GetFileName(selectedPath)}", isError: false);
DisplayCertificateInfo(certificateInfo);
UpdateToNextStep(2);
SetFileButtonState(fileLoaded: true);
RefreshActionButtonStates();
// Zielpfade mit Quell-Kennwort erneut prüfen, damit Ablauf/Gültigkeit sichtbar wird
if (_loadedTargets.Count > 0)
{
_ = ProbeTargetCertificatesAsync();
}
}
private CertificateInfo? TryReadSelectedCertificate(
string selectedPath,
out string? certificatePassword)
{
certificatePassword = null;
string extension = Path.GetExtension(selectedPath);
bool needsPassword =
extension.Equals(".pfx", StringComparison.OrdinalIgnoreCase)
|| extension.Equals(".p12", StringComparison.OrdinalIgnoreCase);
try
{
// Zuerst ohne Passwort versuchen
return ReadCertificate(selectedPath, null);
}
catch (CryptographicException) when (needsPassword)
{
// Passwort nur abfragen, wenn das Laden ohne Passwort fehlgeschlagen ist
string? pfxPassword = PromptForPfxPassword();
if (pfxPassword == null)
{
return null;
}
try
{
CertificateInfo info = ReadCertificate(selectedPath, pfxPassword);
certificatePassword = pfxPassword;
return info;
}
catch (CryptographicException)
{
MessageBox.Show(
"Falsches Kennwort oder Datei beschädigt.",
"Zertifikat",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
return null;
}
}
catch (Exception ex)
{
MessageBox.Show(
this,
"Zertifikat konnte nicht gelesen werden.",
"Zertifikat",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
return null;
}
}
private void ClearSelectedCertificate()
{
txtCertificatePath.Text = string.Empty;
_loadedCertificateInfo = null;
_loadedCertificatePassword = null;
SetStatus("Keine Datei ausgewählt", isError: false);
lblCertificateSubject.Text = "-";
lblCertificateIssuer.Text = "-";
lblCertificateExpiry.Text = "-";
lblCertificateFingerprint.Text = "-";
SetValidityBadge(
"○ NICHT GELADEN",
MutedTextColor,
Color.FromArgb(39, 52, 73));
UpdateToNextStep(1);
SetFileButtonState(fileLoaded: false);
RefreshActionButtonStates();
}
private void SetFileButtonState(bool fileLoaded)
{
isCertificateFileLoaded = fileLoaded;
if (fileLoaded)
{
btnSelectFile.Text = "Datei auswerfen";
btnSelectFile.BackColor = Color.FromArgb(72, 31, 38);
btnSelectFile.ForeColor = Color.FromArgb(255, 210, 210);
btnSelectFile.FlatAppearance.BorderColor = RedColor;
btnSelectFile.FlatAppearance.MouseOverBackColor = Color.FromArgb(96, 42, 50);
return;
}
btnSelectFile.Text = "Datei auswählen";
btnSelectFile.BackColor = Color.FromArgb(39, 52, 73);
btnSelectFile.ForeColor = TextColor;
btnSelectFile.FlatAppearance.BorderColor = BorderColor;
btnSelectFile.FlatAppearance.MouseOverBackColor = Color.FromArgb(54, 69, 94);
}
private void SetValidityBadge(string text, Color foreColor, Color backColor)
{
lblCertificateValidity.Text = text;
lblCertificateValidity.ForeColor = foreColor;
lblCertificateValidity.BackColor = backColor;
RepositionValidityBadge();
}
private void RepositionValidityBadge()
{
if (lblCertificateValidity?.Parent == null)
{
return;
}
lblCertificateValidity.Left =
lblCertificateValidity.Parent.Width - lblCertificateValidity.Width - 22;
}
private static string? PromptForPfxPassword()
{
using Form dialog = new Form
{
Text = "PFX-Passwort eingeben",
Size = new Size(360, 150),
StartPosition = FormStartPosition.CenterParent,
FormBorderStyle = FormBorderStyle.FixedDialog,
MinimizeBox = false,
MaximizeBox = false
};
Label label = new Label
{
Text = "Passwort für die PFX-Datei:",
Location = new Point(12, 15),
AutoSize = true
};
TextBox txtPwd = new TextBox
{
Location = new Point(12, 38),
Width = 320,
UseSystemPasswordChar = true
};
Button btnOk = new Button
{
Text = "OK",
DialogResult = DialogResult.OK,
Location = new Point(176, 72),
Width = 75
};
Button btnCancel = new Button
{
Text = "Abbrechen",
DialogResult = DialogResult.Cancel,
Location = new Point(257, 72),
Width = 75
};
dialog.Controls.AddRange([label, txtPwd, btnOk, btnCancel]);
dialog.AcceptButton = btnOk;
dialog.CancelButton = btnCancel;
return dialog.ShowDialog() == DialogResult.OK ? txtPwd.Text : null;
}
private static CertificateInfo ReadCertificate(string certificatePath, string? pfxPassword = null)
{
if (string.IsNullOrWhiteSpace(certificatePath))
{
throw new ArgumentException(
"Es wurde kein Zertifikatspfad angegeben.",
nameof(certificatePath));
}
if (!File.Exists(certificatePath))
{
throw new FileNotFoundException(
"Die ausgewählte Zertifikatsdatei wurde nicht gefunden.",
certificatePath);
}
string extension = Path.GetExtension(certificatePath);
bool passwordProtected =
extension.Equals(".pfx", StringComparison.OrdinalIgnoreCase)
|| extension.Equals(".p12", StringComparison.OrdinalIgnoreCase);
using X509Certificate2 certificate =
passwordProtected
? new X509Certificate2(
certificatePath,
pfxPassword,
X509KeyStorageFlags.EphemeralKeySet)
: new X509Certificate2(certificatePath);
string subject = certificate.GetNameInfo(
X509NameType.SimpleName,
forIssuer: false);
string issuer = certificate.GetNameInfo(
X509NameType.SimpleName,
forIssuer: true);
if (string.IsNullOrWhiteSpace(subject))
{
subject = certificate.Subject;
}
if (string.IsNullOrWhiteSpace(issuer))
{
issuer = certificate.Issuer;
}
string fingerprint = certificate.GetCertHashString(
HashAlgorithmName.SHA256);
fingerprint = FormatFingerprint(fingerprint);
DateTimeOffset validFrom =
new DateTimeOffset(certificate.NotBefore);
DateTimeOffset validUntil =
new DateTimeOffset(certificate.NotAfter);
DateTimeOffset now = DateTimeOffset.Now;
bool isCurrentlyValid =
now >= validFrom &&
now <= validUntil;
return new CertificateInfo
{
Subject = subject,
Issuer = issuer,
ValidFrom = validFrom,
ValidUntil = validUntil,
FingerprintSha256 = fingerprint,
IsCurrentlyValid = isCurrentlyValid
};
}
private void DisplayCertificateInfo(CertificateInfo certificateInfo)
{
lblCertificateSubject.Text = certificateInfo.Subject;
lblCertificateIssuer.Text = certificateInfo.Issuer;
lblCertificateExpiry.Text =
certificateInfo.ValidUntil.ToLocalTime().ToString("dd.MM.yyyy HH:mm");
lblCertificateFingerprint.Text = certificateInfo.FingerprintSha256;
if (certificateInfo.IsCurrentlyValid)
{
SetValidityBadge("● GÜLTIG", GreenColor, Color.FromArgb(20, 62, 46));
return;
}
SetValidityBadge(
"● ABGELAUFEN / NICHT GÜLTIG",
RedColor,
Color.FromArgb(72, 31, 38));
}
private static string FormatFingerprint(string fingerprint)
{
if (string.IsNullOrWhiteSpace(fingerprint))
{
return string.Empty;
}
return string.Join(
":",
Enumerable.Range(0, fingerprint.Length / 2)
.Select(index => fingerprint.Substring(index * 2, 2)));
}
private static bool IsSupportedCertificateFile(string filePath)
{
if (string.IsNullOrWhiteSpace(filePath))
{
return false;
}
if (!File.Exists(filePath))
{
return false;
}
string extension = Path.GetExtension(filePath);
string[] supportedExtensions =
{
".cer",
".crt",
".pem",
".pfx",
".p12"
};
return supportedExtensions.Contains(
extension,
StringComparer.OrdinalIgnoreCase);
}
private void BuildDetailsCard(Panel card)
{
Label step = CreateSmallTitle("ERKANNTE INFORMATIONEN");
step.Location = new Point(22, 18);
Label title = CreateCardTitle("Zertifikatsdetails");
title.Location = new Point(22, 40);
lblCertificateValidity = new Label
{
Text = "○ NICHT GELADEN",
ForeColor = MutedTextColor,
BackColor = Color.FromArgb(39, 52, 73),
Font = new Font("Segoe UI", 8, FontStyle.Bold),
AutoSize = true,
Padding = new Padding(8, 5, 8, 5),
Anchor = AnchorStyles.Top | AnchorStyles.Right
};
lblCertificateValidity.Location = new Point(300, 40);
card.Resize += (_, _) => RepositionValidityBadge();
Label subjectLabel = CreateMutedLabel("SUBJECT / CN");
subjectLabel.Location = new Point(22, 84);
lblCertificateSubject = CreateValueLabel("-");
lblCertificateSubject.Location = new Point(22, 102);
lblCertificateSubject.AutoEllipsis = true;
lblCertificateSubject.MaximumSize = new Size(210, 0);
Label issuerLabel = CreateMutedLabel("AUSSTELLER");
issuerLabel.Location = new Point(22, 133);
lblCertificateIssuer = CreateValueLabel("-");
lblCertificateIssuer.Location = new Point(22, 151);
lblCertificateIssuer.AutoEllipsis = true;
lblCertificateIssuer.MaximumSize = new Size(210, 0);
Label expiryLabel = CreateMutedLabel("GÜLTIG BIS");
expiryLabel.Location = new Point(22, 182);
lblCertificateExpiry = CreateValueLabel("-");
lblCertificateExpiry.Location = new Point(22, 200);
Label fingerprintLabel = CreateMutedLabel("SHA-256 FINGERPRINT");
fingerprintLabel.Location = new Point(245, 84);
lblCertificateFingerprint = CreateValueLabel("-");
lblCertificateFingerprint.Location = new Point(245, 102);
lblCertificateFingerprint.AutoSize = false;
lblCertificateFingerprint.Size = new Size(310, 48);
card.Controls.Add(step);
card.Controls.Add(title);
card.Controls.Add(lblCertificateValidity);
card.Controls.Add(subjectLabel);
card.Controls.Add(lblCertificateSubject);
card.Controls.Add(issuerLabel);
card.Controls.Add(lblCertificateIssuer);
card.Controls.Add(expiryLabel);
card.Controls.Add(lblCertificateExpiry);
card.Controls.Add(fingerprintLabel);
card.Controls.Add(lblCertificateFingerprint);
}
private Panel BuildTargetsCard()
{
Panel card = CreateCard();
Label step = CreateSmallTitle("SCHRITT 2");
step.Location = new Point(22, 18);
Label title = CreateCardTitle("Bereitstellungsziele");
title.Location = new Point(22, 40);
Label description = CreateMutedLabel(
"Wähle aus, auf welche Systeme das Zertifikat verteilt werden soll.");
description.Location = new Point(22, 72);
dgvTargets = new DataGridView
{
Location = new Point(22, 148),
Size = new Size(1000, 205),
Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right,
BackgroundColor = CardColor,
BorderStyle = BorderStyle.None,
EnableHeadersVisualStyles = false,
AllowUserToAddRows = false,
AllowUserToDeleteRows = false,
AllowUserToResizeRows = false,
AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill,
RowHeadersVisible = false,
SelectionMode = DataGridViewSelectionMode.FullRowSelect,
MultiSelect = false,
GridColor = BorderColor
};
dgvTargets.ColumnHeadersDefaultCellStyle = new DataGridViewCellStyle
{
BackColor = Color.FromArgb(24, 34, 52),
ForeColor = MutedTextColor,
Font = new Font("Segoe UI", 8.5f, FontStyle.Bold),
Alignment = DataGridViewContentAlignment.MiddleLeft
};
dgvTargets.DefaultCellStyle = new DataGridViewCellStyle
{
BackColor = CardColor,
ForeColor = TextColor,
SelectionBackColor = Color.FromArgb(40, 57, 82),
SelectionForeColor = TextColor,
Font = new Font("Segoe UI", 9),
Padding = new Padding(4, 0, 4, 0)
};
dgvTargets.AlternatingRowsDefaultCellStyle = new DataGridViewCellStyle
{
BackColor = Color.FromArgb(28, 39, 57),
ForeColor = TextColor
};
dgvTargets.ColumnHeadersHeight = 34;
dgvTargets.RowTemplate.Height = 32;
dgvTargets.Columns.Add(new DataGridViewCheckBoxColumn
{
Name = "Selected",
HeaderText = "",
FillWeight = 25
});
dgvTargets.Columns.Add("Target", "Ziel");
dgvTargets.Columns.Add("Path", "Zertifikat-Pfad");
dgvTargets.Columns.Add("Expiry", "Aktuelles Zertifikat");
dgvTargets.Columns.Add("Container", "Neustart-Container");
dgvTargets.Columns.Add("Status", "Status");
DataGridViewButtonColumn actionsColumn = new DataGridViewButtonColumn
{
Name = "Actions",
HeaderText = "",
Text = "⋮",
UseColumnTextForButtonValue = true,
FillWeight = 18,
FlatStyle = FlatStyle.Flat
};
actionsColumn.DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter;
actionsColumn.DefaultCellStyle.ForeColor = MutedTextColor;
actionsColumn.DefaultCellStyle.SelectionForeColor = TextColor;
dgvTargets.Columns.Add(actionsColumn);
_targetRowMenu = new ContextMenuStrip();
ToolStripMenuItem restartMenuItem = new ToolStripMenuItem("Manuell neu starten");
restartMenuItem.Click += async (_, _) =>
{
if (_targetRowMenu.Tag is DeploymentTarget target)
{
await RunRestartOnlyAsync([target]);
}
};
_targetRowMenu.Items.Add(restartMenuItem);
dgvTargets.CellContentClick += (_, e) =>
{
if (e.RowIndex < 0
|| e.ColumnIndex < 0
|| dgvTargets.Columns[e.ColumnIndex].Name != "Actions")
{
return;
}
if (dgvTargets.Rows[e.RowIndex].Tag is not DeploymentTarget target)
{
return;
}
_targetRowMenu.Tag = target;
Rectangle cellRect = dgvTargets.GetCellDisplayRectangle(
e.ColumnIndex,
e.RowIndex,
true);
Point screenLocation = dgvTargets.PointToScreen(
new Point(cellRect.Left, cellRect.Bottom));
_targetRowMenu.Show(screenLocation);
};
dgvTargets.CurrentCellDirtyStateChanged += (_, _) =>
{
if (dgvTargets.IsCurrentCellDirty
&& dgvTargets.CurrentCell is DataGridViewCheckBoxCell)
{
dgvTargets.CommitEdit(DataGridViewDataErrorContexts.Commit);
}
};
dgvTargets.CellValueChanged += (_, e) =>
{
if (e.ColumnIndex >= 0
&& dgvTargets.Columns[e.ColumnIndex].Name == "Selected")
{
RefreshActionButtonStates();
}
};
card.Resize += (_, _) =>
{
dgvTargets.Width = card.Width - 44;
};
card.Controls.Add(step);
card.Controls.Add(title);
card.Controls.Add(description);
card.Controls.Add(dgvTargets);
// ---- Sonic Management Console Discovery ----
BuildSonicDiscoveryRow(card);
return card;
}
private Panel BuildActionPanel()
{
Panel panel = new Panel
{
BackColor = BackgroundColor
};
btnCancelRun = CreateSecondaryButton("Abbrechen");
btnCancelRun.Size = new Size(120, 42);
btnCancelRun.Anchor = AnchorStyles.Top | AnchorStyles.Right;
btnCancelRun.Enabled = false;
btnCancelRun.Click += (_, _) =>
{
_runCts?.Cancel();
SetStatus("Abbruch angefordert…", isError: false);
};
btnDeploy = CreatePrimaryButton("Austauschen + Neustart");
btnDeploy.Size = new Size(220, 42);
btnDeploy.Anchor = AnchorStyles.Top | AnchorStyles.Right;
btnDeploy.Click += async (_, _) => await RunDeploymentAsync();
panel.Controls.Add(btnCancelRun);
panel.Controls.Add(btnDeploy);
void PositionActionButtons()
{
const int spacing = 12;
const int rightMargin = 0;
btnDeploy.Left = panel.ClientSize.Width - btnDeploy.Width - rightMargin;
btnCancelRun.Left = btnDeploy.Left - btnCancelRun.Width - spacing;
}
panel.Resize += (_, _) => PositionActionButtons();
PositionActionButtons();
RefreshActionButtonStates();
return panel;
}
private Panel BuildStatusBar()
{
Panel panel = new Panel
{
Height = 28,
BackColor = BackgroundColor
};
lblStatusDot = new Label
{
Text = "●",
ForeColor = GreenColor,
Font = new Font("Segoe UI", 10),
AutoSize = true,
Location = new Point(0, 4)
};
lblStatus = new Label
{
Text = "Warte auf Zertifikatsauswahl",
ForeColor = MutedTextColor,
Font = new Font("Segoe UI", 9),
AutoSize = true,
Location = new Point(18, 5)
};
panel.Controls.Add(lblStatusDot);
panel.Controls.Add(lblStatus);
return panel;
}
private async Task LoadTargetsAsync()
{
try
{
SetStatus(
"Bereitstellungsziele werden aus SQL geladen ...",
isError: false);
IReadOnlyList<DeploymentTarget> fromSql =
await _deploymentTargetRepository.GetActiveAsync(
_settings.EnvironmentCode);
_loadedTargets = EnsureDemoTarget(fromSql);
BindTargetsToGrid(_loadedTargets);
if (_loadedTargets.Count == 0)
{
SetStatus(
"In SQL sind keine aktiven Bereitstellungsziele konfiguriert.",
isError: true);
return;
}
// Demo: erstes Ziel vorauswählen
if (dgvTargets.Rows.Count > 0)
{
dgvTargets.Rows[0].Cells["Selected"].Value = true;
}
SetStatus(
$"{_loadedTargets.Count} Bereitstellungsziel(e) geladen"
+ (_settings.Demo.Enabled
? $" · Demo: {Path.Combine(_settings.Demo.TargetDirectory, _settings.Demo.TargetFileName)}"
: string.Empty),
isError: false);
await ProbeTargetCertificatesAsync();
}
catch (Exception ex) when (_settings.Demo.Enabled)
{
// Demo trotzdem möglich, wenn SQL kurz nicht erreichbar ist
_loadedTargets = EnsureDemoTarget([]);
BindTargetsToGrid(_loadedTargets);
if (dgvTargets.Rows.Count > 0)
{
dgvTargets.Rows[0].Cells["Selected"].Value = true;
}
SetStatus(
$"SQL-Ziele nicht geladen ({ex.Message}). "
+ "Demo-Pfad ist trotzdem verfügbar.",
isError: true);
await ProbeTargetCertificatesAsync();
}
catch
{
_loadedTargets = [];
dgvTargets.Rows.Clear();
throw;
}
finally
{
RefreshActionButtonStates();
}
}
private void BindTargetsToGrid(IReadOnlyList<DeploymentTarget> targets)
{
dgvTargets.Rows.Clear();
foreach (DeploymentTarget target in targets)
{
int rowIndex = dgvTargets.Rows.Add(
false,
target.Name,
target.FullTargetPath,
"wird geprüft …",
string.IsNullOrWhiteSpace(target.ContainerName)
? "—"
: target.ContainerName,
"Bereit");
dgvTargets.Rows[rowIndex].Tag = target;
}
}
private IReadOnlyList<DeploymentTarget> EnsureDemoTarget(
IReadOnlyList<DeploymentTarget> fromSql)
{
if (!_settings.Demo.Enabled)
{
return fromSql;
}
string directory = _settings.Demo.TargetDirectory.Trim();
string fileName = _settings.Demo.TargetFileName.Trim();
if (string.IsNullOrWhiteSpace(directory)
|| string.IsNullOrWhiteSpace(fileName))
{
return fromSql;
}
bool alreadyPresent = fromSql.Any(target =>
string.Equals(
target.TargetDirectory.TrimEnd('\\', '/'),
directory.TrimEnd('\\', '/'),
StringComparison.OrdinalIgnoreCase)
&& string.Equals(
target.CertificateFileName,
fileName,
StringComparison.OrdinalIgnoreCase));
if (alreadyPresent)
{
return fromSql;
}
List<DeploymentTarget> merged = [.. fromSql];
merged.Insert(
0,
new DeploymentTarget
{
Id = -1,
Name = _settings.Demo.DisplayName,
Environment = _settings.EnvironmentCode,
IsActive = true,
TargetDirectory = directory,
CertificateFileName = fileName,
BackupEnabled = true,
BackupDirectoryName = "Backup",
ContainerName = _settings.Demo.ContainerName,
RestartType = RestartType.None,
SonicConnectionName =
_settings.Demo.SonicConnectionName,
RestartTimeoutSeconds = 180
});
return merged;
}
private async Task ProbeTargetCertificatesAsync(
IReadOnlySet<int>? preferLoadedCertificateForTargetIds = null)
{
CertificateProbeService fileProbe = new();
TlsEndpointProbeService tlsProbe = new();
string? password = _loadedCertificatePassword;
Func<string?>? passwordPrompt = password is null
? null
: () => password;
foreach (DataGridViewRow row in dgvTargets.Rows)
{
if (row.Tag is not DeploymentTarget target)
{
continue;
}
// Bevorzugt Live-TLS-Probe (curl-ähnlich), wenn URL am Container gesetzt.
if (!string.IsNullOrWhiteSpace(target.TlsHost)
&& target.TlsPort is int tlsPort)
{
string checkUrl =
string.IsNullOrWhiteSpace(target.TlsServerName)
? $"{target.TlsHost}:{tlsPort}"
: $"https://{target.TlsServerName}:{tlsPort}";
CertificateProbeResult tlsResult = await Task.Run(
() => tlsProbe.Probe(checkUrl));
if (IsDisposed || !dgvTargets.Columns.Contains("Expiry"))
{
return;
}
if (tlsResult.Success && tlsResult.Certificate is not null)
{
row.Cells["Expiry"].Value =
"TLS · "
+ FormatCertificateExpiryCell(tlsResult.Certificate);
continue;
}
row.Cells["Expiry"].Value = "TLS · nicht erreichbar";
continue;
}
CertificateProbeResult result = await Task.Run(
() => fileProbe.Probe(target.FullTargetPath, passwordPrompt));
if (IsDisposed || !dgvTargets.Columns.Contains("Expiry"))
{
return;
}
if (result.Success && result.Certificate is not null)
{
row.Cells["Expiry"].Value = FormatCertificateExpiryCell(
result.Certificate);
continue;
}
// Nach erfolgreichem Tausch: Quellzertifikat liegt am Ziel
// Gültigkeit auch zeigen, falls Probe am Kennwort scheitert.
if (preferLoadedCertificateForTargetIds is not null
&& preferLoadedCertificateForTargetIds.Contains(target.Id)
&& _loadedCertificateInfo is not null
&& result.FileExists)
{
row.Cells["Expiry"].Value = FormatCertificateExpiryCell(
_loadedCertificateInfo);
continue;
}
if (result.FileExists)
{
row.Cells["Expiry"].Value = "Datei da · Kennwort nötig";
continue;
}
if (result.AccessDenied)
{
row.Cells["Expiry"].Value =
CertificateProbeResult.AccessDeniedShortText;
continue;
}
row.Cells["Expiry"].Value = result.Success
? "Datei fehlt"
: "nicht erreichbar";
}
}
private static string FormatCertificateExpiryCell(CertificateInfo certificate)
{
string expiry = certificate.ValidUntil
.ToLocalTime()
.ToString("dd.MM.yyyy");
return certificate.IsCurrentlyValid
? $"gültig bis {expiry}"
: $"abgelaufen {expiry}";
}
private List<DeploymentTarget> GetSelectedTargets()
{
List<DeploymentTarget> selected = [];
foreach (DataGridViewRow row in dgvTargets.Rows)
{
if (row.Cells["Selected"].Value is true && row.Tag is DeploymentTarget target)
{
selected.Add(target);
}
}
return selected;
}
private void SetTargetRowStatus(int targetId, string status)
{
foreach (DataGridViewRow row in dgvTargets.Rows)
{
if (row.Tag is DeploymentTarget target && target.Id == targetId)
{
row.Cells["Status"].Value = status;
break;
}
}
}
private void RefreshActionButtonStates()
{
if (btnDeploy is null || btnCancelRun is null)
{
return;
}
bool idle = !_isOperationRunning;
bool hasCertificate = isCertificateFileLoaded && _loadedCertificateInfo is not null;
bool hasSelection = dgvTargets is not null && GetSelectedTargets().Count > 0;
if (btnSelectFile is not null)
{
btnSelectFile.Enabled = idle;
}
btnCancelRun.Enabled = _isOperationRunning;
btnDeploy.Enabled = idle && hasCertificate && hasSelection;
if (dgvTargets is not null)
{
dgvTargets.Enabled = idle;
}
}
private void SetStatus(string message, bool isError)
{
if (lblStatus is null)
{
return;
}
lblStatus.Text = message;
lblStatus.ForeColor = isError ? RedColor : MutedTextColor;
if (lblStatusDot is not null)
{
lblStatusDot.ForeColor = isError ? RedColor : (_isOperationRunning ? GoldColor : GreenColor);
}
}
private void SetOperationRunning(bool running)
{
_isOperationRunning = running;
RefreshActionButtonStates();
}
private void ShowPreflightIssues(PreflightValidationResult result, string statusMessage, string caption)
{
string details = string.Join(Environment.NewLine, result.Issues.Select(i => "• " + i.Message));
SetStatus(statusMessage, isError: true);
MessageBox.Show(this, details, caption, MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
private async Task RunRestartOnlyAsync(IReadOnlyList<DeploymentTarget> targets)
{
if (_orchestrator is null)
{
SetStatus(
"Die Anwendung ist noch nicht vollständig initialisiert.",
isError: true);
return;
}
if (_isOperationRunning)
{
return;
}
List<DeploymentTarget> selected = [.. targets];
if (selected.Count == 0)
{
SetStatus("Kein Ziel für den Neustart ausgewählt.", isError: true);
return;
}
PreflightValidationResult preflight = _orchestrator.ValidateRestartOnly(selected);
if (!preflight.IsValid)
{
ShowPreflightIssues(
preflight,
"Neustart blockiert Vorabprüfung fehlgeschlagen.",
"Manuell neu starten");
return;
}
string targetLabel = selected.Count == 1
? selected[0].Name
: $"{selected.Count} Ziele";
DialogResult confirm = MessageBox.Show(
this,
$"Container für „{targetLabel}“ neu starten?",
"Manuell neu starten",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning);
if (confirm != DialogResult.Yes)
{
return;
}
_runCts?.Dispose();
_runCts = new CancellationTokenSource();
SetOperationRunning(true);
UpdateToNextStep(3);
SetStatus($"Neustart läuft für {selected.Count} Ziel(e)…", isError: false);
Progress<TargetProgressUpdate> progress = new(update =>
{
SetTargetRowStatus(update.TargetId, update.StatusText);
SetStatus(update.StatusText, isError: update.SuccessHint == false);
});
try
{
DeploymentRunResult runResult = await _orchestrator.RestartOnlyAsync(
selected,
progress,
_runCts.Token);
foreach (TargetStepResult targetResult in runResult.TargetResults)
{
SetTargetRowStatus(targetResult.TargetId, targetResult.StatusText);
}
SetStatus(
runResult.OverallSuccess
? $"Neustart verifiziert ({runResult.TargetResults.Count} Ziel(e))."
: "Neustart fehlgeschlagen oder nicht verifiziert.",
isError: !runResult.OverallSuccess);
if (runResult.OverallSuccess)
{
UpdateToNextStep(4);
HashSet<int> restartedTargetIds = runResult.TargetResults
.Where(result => result.Success)
.Select(result => result.TargetId)
.ToHashSet();
await ProbeTargetCertificatesAsync(restartedTargetIds);
}
else
{
string details = string.Join(
Environment.NewLine + Environment.NewLine,
runResult.TargetResults
.Where(result => !result.Success)
.Select(result =>
$"{result.TargetName}"
+ Environment.NewLine
+ SonicErrorClassifier.FormatRestartFailure(
result.StatusText,
result.Detail)));
bool permissionIssue =
SonicErrorClassifier.LooksLikeMissingPermission(details);
if (permissionIssue)
{
SetStatus(
"Neustart nicht möglich fehlende Benutzerrechte.",
isError: true);
}
CopyableErrorDialog.Show(
this,
permissionIssue
? "Keine Rechte für den Neustart"
: "Neustart fehlgeschlagen (MfApi)",
string.IsNullOrWhiteSpace(details)
? "Neustart fehlgeschlagen (keine Details)."
: details);
}
}
catch (OperationCanceledException)
{
SetStatus("Neustart abgebrochen.", isError: true);
}
catch (Exception ex)
{
string formatted =
SonicErrorClassifier.FormatRestartFailure(
"Neustart fehlgeschlagen",
ex.ToString());
bool permissionIssue =
SonicErrorClassifier.LooksLikeMissingPermission(formatted);
SetStatus(
permissionIssue
? "Neustart nicht möglich fehlende Benutzerrechte."
: $"Neustart fehlgeschlagen: {ex.Message}",
isError: true);
CopyableErrorDialog.Show(
this,
permissionIssue
? "Keine Rechte für den Neustart"
: "Neustart fehlgeschlagen (MfApi)",
formatted);
}
finally
{
SetOperationRunning(false);
_runCts?.Dispose();
_runCts = null;
}
}
private async Task RunDeploymentAsync()
{
if (_orchestrator is null)
{
SetStatus(
"Die Anwendung ist noch nicht vollständig initialisiert.",
isError: true);
return;
}
if (_isOperationRunning)
{
return;
}
List<DeploymentTarget> selectedTargets = GetSelectedTargets();
if (_loadedCertificateInfo is null ||
string.IsNullOrWhiteSpace(txtCertificatePath.Text) ||
!File.Exists(txtCertificatePath.Text))
{
SetStatus(
"Bitte zuerst eine gültige Zertifikatsdatei auswählen.",
isError: true);
return;
}
if (selectedTargets.Count == 0)
{
SetStatus(
"Bitte mindestens ein Ziel auswählen.",
isError: true);
return;
}
DialogResult confirm = MessageBox.Show(
this,
"Zertifikat austauschen?",
"Austauschen + Neustart",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning);
if (confirm != DialogResult.Yes)
{
return;
}
_runCts?.Dispose();
_runCts = new CancellationTokenSource();
SetOperationRunning(true);
UpdateToNextStep(3);
SetStatus(
$"Tausche Zertifikat auf {selectedTargets.Count} Ziel(e) und starte Container neu …",
isError: false);
Progress<TargetProgressUpdate> progress = new(update =>
{
SetTargetRowStatus(update.TargetId, update.StatusText);
SetStatus(update.StatusText, isError: update.SuccessHint == false);
});
try
{
DeploymentRunResult runResult = await _orchestrator.DeployAsync(
txtCertificatePath.Text,
_loadedCertificateInfo.FingerprintSha256,
selectedTargets,
progress,
_runCts.Token);
foreach (TargetStepResult targetResult in runResult.TargetResults)
{
SetTargetRowStatus(
targetResult.TargetId,
targetResult.StatusText);
}
HashSet<int> exchangedTargetIds = runResult.TargetResults
.Where(result => result.Success)
.Select(result => result.TargetId)
.ToHashSet();
if (runResult.OverallSuccess)
{
SetStatus(
$"Austausch + Neustart OK ({runResult.TargetResults.Count} Ziel(e)).",
isError: false);
UpdateToNextStep(4);
await ProbeTargetCertificatesAsync(exchangedTargetIds);
}
else
{
SetStatus(
"Austausch für mindestens ein Ziel fehlgeschlagen.",
isError: true);
string details = string.Join(
Environment.NewLine + Environment.NewLine,
runResult.TargetResults
.Where(result => !result.Success)
.Select(result =>
$"{result.TargetName}"
+ Environment.NewLine
+ SonicErrorClassifier.FormatRestartFailure(
result.StatusText,
result.Detail)));
bool permissionIssue =
SonicErrorClassifier.LooksLikeMissingPermission(details);
if (permissionIssue)
{
SetStatus(
"Austausch/Neustart nicht möglich fehlende Benutzerrechte.",
isError: true);
}
CopyableErrorDialog.Show(
this,
permissionIssue
? "Keine Rechte für den Neustart"
: "Austausch fehlgeschlagen",
details);
await ProbeTargetCertificatesAsync(exchangedTargetIds);
}
}
catch (OperationCanceledException)
{
SetStatus("Austausch abgebrochen.", isError: true);
}
catch (Exception ex)
{
SetStatus(
$"Austausch fehlgeschlagen: {ex.Message}",
isError: true);
CopyableErrorDialog.Show(
this,
"Austausch fehlgeschlagen",
ex.ToString());
}
finally
{
SetOperationRunning(false);
_runCts?.Dispose();
_runCts = null;
}
}
private Panel CreateCard()
{
return new Panel
{
BackColor = CardColor,
BorderStyle = BorderStyle.FixedSingle,
Padding = new Padding(20)
};
}
private Label CreateSmallTitle(string text)
{
return new Label
{
Text = text,
ForeColor = BlueColor,
Font = new Font("Segoe UI", 8, FontStyle.Bold),
AutoSize = true
};
}
private Label CreateCardTitle(string text)
{
return new Label
{
Text = text,
ForeColor = TextColor,
Font = new Font("Segoe UI", 14, FontStyle.Bold),
AutoSize = true
};
}
private Label CreateMutedLabel(string text)
{
return new Label
{
Text = text,
ForeColor = MutedTextColor,
Font = new Font("Segoe UI", 9),
AutoSize = true
};
}
private Label CreateValueLabel(string text)
{
return new Label
{
Text = text,
ForeColor = TextColor,
Font = new Font("Segoe UI", 9.5f, FontStyle.Regular),
AutoSize = true
};
}
private Button CreatePrimaryButton(string text)
{
Button button = new Button
{
Text = text,
BackColor = GoldColor,
ForeColor = Color.FromArgb(30, 25, 10),
FlatStyle = FlatStyle.Flat,
FlatAppearance =
{
BorderSize = 0,
MouseOverBackColor = Color.FromArgb(250, 204, 21)
},
Font = new Font("Segoe UI", 9.5f, FontStyle.Bold),
Cursor = Cursors.Hand
};
return button;
}
private Button CreateSecondaryButton(string text)
{
Button button = new Button
{
Text = text,
BackColor = Color.FromArgb(39, 52, 73),
ForeColor = TextColor,
FlatStyle = FlatStyle.Flat,
FlatAppearance =
{
BorderColor = BorderColor,
BorderSize = 1,
MouseOverBackColor = Color.FromArgb(54, 69, 94)
},
Font = new Font("Segoe UI", 9, FontStyle.Bold),
Cursor = Cursors.Hand
};
return button;
}
// ---------------------------------------------------------------
// Sonic Status (KnownContainers aus appsettings)
// ---------------------------------------------------------------
private async Task LoadCredentialsForConnectionsAsync()
{
_credentialsByConnectionId.Clear();
foreach (DatabaseSonicConnection connection
in _databaseSonicConnections)
{
IReadOnlyList<SonicCredentialProfile> profiles =
await _sonicSetupRepository.GetCredentialsAsync(
connection.Id);
SonicCredentialProfile? selected = null;
if (_localSetupSelection is not null
&& _localSetupSelection.SonicConnectionId
== connection.Id
&& _localSetupSelection.SonicCredentialId > 0)
{
selected = profiles.FirstOrDefault(
profile =>
profile.SonicCredentialId
== _localSetupSelection.SonicCredentialId);
}
selected ??=
profiles.FirstOrDefault(profile => profile.IsDefault)
?? profiles.FirstOrDefault();
if (selected is not null && selected.IsComplete)
{
_credentialsByConnectionId[connection.Id] =
selected;
}
}
}
private void BindDatabaseSonicConnections()
{
cmbSonicConnection.Items.Clear();
foreach (DatabaseSonicConnection connection
in _databaseSonicConnections)
{
cmbSonicConnection.Items.Add(connection.Name);
}
if (cmbSonicConnection.Items.Count == 0)
{
RefreshSonicStatusLine();
return;
}
int preferredIndex = 0;
if (_localSetupSelection is not null
&& !string.IsNullOrWhiteSpace(
_localSetupSelection.ConnectionName))
{
for (int index = 0;
index < cmbSonicConnection.Items.Count;
index++)
{
if (string.Equals(
cmbSonicConnection.Items[index]?.ToString(),
_localSetupSelection.ConnectionName,
StringComparison.OrdinalIgnoreCase))
{
preferredIndex = index;
break;
}
}
}
cmbSonicConnection.SelectedIndex = preferredIndex;
}
private void BuildSonicDiscoveryRow(Panel card)
{
Label sonicLabel = new Label
{
Text = "Sonic Verbindung:",
ForeColor = MutedTextColor,
Font = new Font("Segoe UI", 8.5f, FontStyle.Bold),
AutoSize = true,
Location = new Point(22, 108)
};
cmbSonicConnection = new ComboBox
{
Location = new Point(160, 104),
Size = new Size(220, 26),
DropDownStyle = ComboBoxStyle.DropDownList,
BackColor = Color.FromArgb(20, 30, 48),
ForeColor = TextColor,
FlatStyle = FlatStyle.Flat,
Font = new Font("Segoe UI", 9)
};
lblSonicStatus = new Label
{
ForeColor = MutedTextColor,
Font = new Font("Segoe UI", 9f, FontStyle.Bold),
AutoSize = true,
MaximumSize = new Size(720, 0),
Location = new Point(392, 108)
};
cmbSonicConnection.SelectedIndexChanged += (_, _) => RefreshSonicStatusLine();
lblSonicStatus.Text = "nicht verbunden";
lblSonicStatus.ForeColor = RedColor;
card.Controls.Add(sonicLabel);
card.Controls.Add(cmbSonicConnection);
card.Controls.Add(lblSonicStatus);
}
private void RefreshSonicStatusLine()
{
if (_databaseSonicConnections.Count == 0)
{
SetSonicConnectionStatus(connected: false);
return;
}
string? selectedName =
cmbSonicConnection.SelectedItem?.ToString();
DatabaseSonicConnection? connection =
_databaseSonicConnections.FirstOrDefault(
item => string.Equals(
item.Name,
selectedName,
StringComparison.OrdinalIgnoreCase));
if (connection is null)
{
SetSonicConnectionStatus(connected: false);
return;
}
try
{
_ = connection.BuildConnectionUrl();
bool hasCredential =
_credentialsByConnectionId.ContainsKey(connection.Id);
SetSonicConnectionStatus(connected: hasCredential);
}
catch (InvalidOperationException)
{
SetSonicConnectionStatus(connected: false);
}
}
private void SetSonicConnectionStatus(bool connected)
{
lblSonicStatus.Text = connected
? "verbunden"
: "nicht verbunden";
lblSonicStatus.ForeColor = connected
? GreenColor
: RedColor;
}
private void Form1_Load(object sender, EventArgs e)
{
}
}
}