Files
123123/ZA.CoreService.ESBCertificateManager/Form1.cs
T
GizzlerandCursor f98daec656 Show target certificate validity after PFX exchange.
Reuse the source PFX password for path probes and fall back to the loaded certificate expiry for successfully exchanged targets.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-31 08:12:45 +02:00

2452 lines
83 KiB
C#
Raw 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 btnValidate = null!;
private Button btnDeploy = null!;
private Button btnRestartOnly = null!;
private Button btnCancelRun = 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,
"Während einer laufenden Aktion können die Einstellungen nicht geöffnet werden.",
"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,
"SQL-Zugriff fehlgeschlagen",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
catch (InvalidOperationException ex)
{
_databaseSonicConnections = [];
SetStatus(
$"Konfiguration ungültig: {ex.Message}",
isError: true);
MessageBox.Show(
this,
ex.Message,
"Konfiguration ungültig",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
}
catch (Exception ex)
{
_databaseSonicConnections = [];
SetStatus(
$"Initialisierung fehlgeschlagen: {ex.Message}",
isError: true);
MessageBox.Show(
this,
ex.Message,
"Initialisierung fehlgeschlagen",
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 + Version
// -----------------------------
Panel footerPanel = new Panel
{
Dock = DockStyle.Bottom,
Height = 118,
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();
Label versionLabel = new Label
{
Text = "INTERNAL TOOL",
ForeColor = MutedTextColor,
Font = new Font("Segoe UI", 7.5f, FontStyle.Bold),
AutoSize = true,
Location = new Point(18, 62)
};
Label versionNumber = new Label
{
Text = "Version 0.1.0",
ForeColor = BlueColor,
Font = new Font("Segoe UI", 8),
AutoSize = true,
Location = new Point(18, 82)
};
footerPanel.Controls.Add(footerSeparator);
footerPanel.Controls.Add(btnSettings);
footerPanel.Controls.Add(versionLabel);
footerPanel.Controls.Add(versionNumber);
// 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)
};
Label environmentBadge = new Label
{
Text = " UMGEBUNG ",
ForeColor = GoldColor,
BackColor = Color.FromArgb(65, 53, 17),
Font = new Font("Segoe UI", 8, FontStyle.Bold),
AutoSize = true,
Padding = new Padding(7, 6, 7, 6),
Anchor = AnchorStyles.Top | AnchorStyles.Right
};
environmentBadge.Location = new Point(900, 9);
header.Resize += (_, _) =>
{
environmentBadge.Left = header.Width - environmentBadge.Width;
};
header.Controls.Add(title);
header.Controls.Add(subtitle);
header.Controls.Add(environmentBadge);
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,
"Bitte wähle eine Zertifikatsdatei in einem der folgenden Formate aus:\n" +
".cer, .crt, .pem, .pfx oder .p12",
"Nicht unterstütztes 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(
"Das angegebene Passwort ist falsch oder die Datei ist beschädigt.",
"Fehler beim Lesen des Zertifikats",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
return null;
}
}
catch (Exception ex)
{
MessageBox.Show(
this,
$"Das Zertifikat konnte nicht gelesen werden.\n\n{ex.Message}",
"Fehler beim Lesen des Zertifikats",
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");
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);
};
btnValidate = CreateSecondaryButton("Prüfung durchführen");
btnValidate.Size = new Size(175, 42);
btnValidate.Anchor = AnchorStyles.Top | AnchorStyles.Right;
btnValidate.Click += async (_, _) => await RunValidationAsync();
// Primäraktion: Neustart (Deploy vorerst deaktiviert Design/Layout bleibt)
btnRestartOnly = CreatePrimaryButton("ESB neu starten");
btnRestartOnly.Size = new Size(170, 42);
btnRestartOnly.Anchor = AnchorStyles.Top | AnchorStyles.Right;
btnRestartOnly.Click += async (_, _) => await RunRestartOnlyAsync();
btnDeploy = CreatePrimaryButton("Austauschen + Neustart");
btnDeploy.Size = new Size(190, 42);
btnDeploy.Anchor = AnchorStyles.Top | AnchorStyles.Right;
btnDeploy.Visible = true;
btnDeploy.Click += async (_, _) => await RunDeploymentAsync();
panel.Controls.Add(btnCancelRun);
panel.Controls.Add(btnValidate);
panel.Controls.Add(btnRestartOnly);
panel.Controls.Add(btnDeploy);
void PositionActionButtons()
{
const int spacing = 12;
const int rightMargin = 0;
// Rechts nach links anordnen:
// Deployment | Neustart | Prüfung | Abbrechen
btnDeploy.Left = panel.ClientSize.Width - btnDeploy.Width - rightMargin;
btnRestartOnly.Left = btnDeploy.Left - btnRestartOnly.Width - spacing;
btnValidate.Left = btnRestartOnly.Left - btnValidate.Width - spacing;
btnCancelRun.Left = btnValidate.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 probe = 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;
}
CertificateProbeResult result = await Task.Run(
() => probe.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;
}
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 (btnValidate is null || btnDeploy is null || btnCancelRun is null)
{
return;
}
bool idle = !_isOperationRunning;
bool hasCertificate = isCertificateFileLoaded && _loadedCertificateInfo is not null;
bool hasTargets = _loadedTargets.Count > 0;
bool hasSelection = dgvTargets is not null && GetSelectedTargets().Count > 0;
if (btnSelectFile is not null)
{
btnSelectFile.Enabled = idle;
}
btnCancelRun.Enabled = _isOperationRunning;
// Prüfung nur für Neustart-Voraussetzungen (Zertifikat optional)
btnValidate.Enabled = idle && hasTargets;
btnDeploy.Visible = true;
btnDeploy.Enabled = idle && hasCertificate && hasSelection;
if (btnRestartOnly is not null)
{
btnRestartOnly.Enabled = idle && 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 Task RunValidationAsync()
{
if (_orchestrator is null)
{
SetStatus(
"Die Anwendung ist noch nicht vollständig initialisiert.",
isError: true);
return Task.CompletedTask;
}
if (_isOperationRunning)
{
return Task.CompletedTask;
}
List<DeploymentTarget> selected = GetSelectedTargets();
PreflightValidationResult result = _orchestrator.ValidateRestartOnly(selected);
HashSet<int> failedTargetIds = result.Issues
.Where(i => i.TargetId is not null)
.Select(i => i.TargetId!.Value)
.ToHashSet();
foreach (DataGridViewRow row in dgvTargets.Rows)
{
if (row.Tag is not DeploymentTarget target)
{
continue;
}
if (failedTargetIds.Contains(target.Id))
{
SetTargetRowStatus(target.Id, "Prüfung fehlgeschlagen");
}
else if (row.Cells["Selected"].Value is true)
{
SetTargetRowStatus(target.Id, "Bereit");
}
}
if (!result.IsValid)
{
ShowPreflightIssues(result, "Vorabprüfung fehlgeschlagen.", "Vorabprüfung");
return Task.CompletedTask;
}
UpdateToNextStep(3);
SetStatus($"Vorabprüfung ok {selected.Count} Ziel(e) bereit.", isError: false);
MessageBox.Show(
this,
$"Alle Voraussetzungen sind erfüllt.\nAusgewählte Ziele: {selected.Count}",
"Vorabprüfung",
MessageBoxButtons.OK,
MessageBoxIcon.Information);
return Task.CompletedTask;
}
private async Task RunRestartOnlyAsync()
{
if (_orchestrator is null)
{
SetStatus(
"Die Anwendung ist noch nicht vollständig initialisiert.",
isError: true);
return;
}
if (_isOperationRunning)
{
return;
}
List<DeploymentTarget> selected = GetSelectedTargets();
PreflightValidationResult preflight = _orchestrator.ValidateRestartOnly(selected);
if (!preflight.IsValid)
{
ShowPreflightIssues(
preflight,
"Neustart blockiert Vorabprüfung fehlgeschlagen.",
"ESB neu starten");
return;
}
string targetNames = string.Join(", ", selected.Select(t =>
string.IsNullOrWhiteSpace(t.ContainerName) ? t.Name : t.ContainerName));
DialogResult confirm = MessageBox.Show(
this,
$"Container wirklich neu starten?\n\n" +
$"Ziele: {targetNames}\n\n",
"ESB neu starten",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning);
if (confirm != DialogResult.Yes)
{
return;
}
_runCts?.Dispose();
_runCts = new CancellationTokenSource();
SetOperationRunning(true);
UpdateToNextStep(3);
SetStatus($"ESB-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);
}
else
{
string details = string.Join(
Environment.NewLine + Environment.NewLine,
runResult.TargetResults
.Where(result => !result.Success)
.Select(result =>
$"{result.TargetName}: {result.StatusText}"
+ Environment.NewLine
+ (result.Detail ?? string.Empty)));
CopyableErrorDialog.Show(
this,
"Neustart fehlgeschlagen (MfApi)",
string.IsNullOrWhiteSpace(details)
? "Neustart fehlgeschlagen (keine Details)."
: details);
}
}
catch (OperationCanceledException)
{
SetStatus("Neustart abgebrochen.", isError: true);
}
catch (Exception ex)
{
SetStatus($"Neustart fehlgeschlagen: {ex.Message}", isError: true);
CopyableErrorDialog.Show(
this,
"Neustart fehlgeschlagen (MfApi)",
ex.ToString());
}
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;
}
string targetSummary = string.Join(
Environment.NewLine,
selectedTargets.Select(target =>
$"• {target.Name}"
+ Environment.NewLine
+ $" Datei → {target.FullTargetPath}"
+ Environment.NewLine
+ $" danach Neustart: {target.ContainerName}"));
DialogResult confirm = MessageBox.Show(
this,
"Zertifikat wirklich austauschen?\n\n"
+ "Ablauf je Ziel:\n"
+ "1) Datei kopieren (Backup der alten Datei)\n"
+ "2) Sonic-Container neu starten (MfApi)\n\n"
+ $"Quelle: {Path.GetFileName(txtCertificatePath.Text)}\n\n"
+ targetSummary,
"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}: {result.StatusText}"
+ Environment.NewLine
+ (result.Detail ?? string.Empty)));
CopyableErrorDialog.Show(
this,
"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", 8.5f),
AutoSize = true,
MaximumSize = new Size(720, 0),
Location = new Point(392, 108)
};
cmbSonicConnection.SelectedIndexChanged += (_, _) => RefreshSonicStatusLine();
lblSonicStatus.Text = "Sonic-Verbindungen werden aus SQL geladen ...";
lblSonicStatus.ForeColor = MutedTextColor;
card.Controls.Add(sonicLabel);
card.Controls.Add(cmbSonicConnection);
card.Controls.Add(lblSonicStatus);
}
private void RefreshSonicStatusLine()
{
if (_databaseSonicConnections.Count == 0)
{
lblSonicStatus.Text =
"Keine aktive Sonic-Verbindung in SQL konfiguriert.";
lblSonicStatus.ForeColor = GoldColor;
return;
}
string? selectedName =
cmbSonicConnection.SelectedItem?.ToString();
DatabaseSonicConnection? connection =
_databaseSonicConnections.FirstOrDefault(
item => string.Equals(
item.Name,
selectedName,
StringComparison.OrdinalIgnoreCase));
if (connection is null)
{
lblSonicStatus.Text =
"Die ausgewählte SQL-Verbindung wurde nicht gefunden.";
lblSonicStatus.ForeColor = RedColor;
return;
}
try
{
string connectionUrl = connection.BuildConnectionUrl();
bool hasCredential =
_credentialsByConnectionId.TryGetValue(
connection.Id,
out SonicCredentialProfile? credential);
string credentialInfo = hasCredential
? $"Profil '{credential!.CredentialName}'"
: "sichere Zugangsdaten noch nicht konfiguriert";
lblSonicStatus.Text =
$"{connection.Name}: {connectionUrl}, {credentialInfo}";
lblSonicStatus.ForeColor = hasCredential
? GreenColor
: GoldColor;
}
catch (InvalidOperationException ex)
{
lblSonicStatus.Text = ex.Message;
lblSonicStatus.ForeColor = RedColor;
}
}
private void Form1_Load(object sender, EventArgs e)
{
}
}
}