Files

1964 lines
68 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.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using ZA.CoreService.ESBCertificateManager.Configuration;
using ZA.CoreService.ESBCertificateManager.Data;
using ZA.CoreService.ESBCertificateManager.Models;
using ZA.CoreService.ESBCertificateManager.Services;
namespace ZA.CoreService.ESBCertificateManager
{
public partial class Form1 : Form
{
// ---------------------------------------------------------
// ZIEHL-ABEGG-inspirierte Dark-Mode-Farbpalette
// ---------------------------------------------------------
// 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 readonly DeploymentOrchestrator _orchestrator;
private readonly SonicContainerDiscovery _sonicDiscovery;
private Label lblStatus = null!;
private Label lblStatusDot = null!;
private TextBox txtCertificatePath = null!;
private DataGridView dgvTargets = null!;
private ComboBox cmbSonicConnection = null!;
private Button btnLoadFromSonic = 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 CertificateInfo? _loadedCertificateInfo;
private IReadOnlyList<DeploymentTarget> _loadedTargets = [];
private bool _isOperationRunning;
private CancellationTokenSource? _runCts;
private int nvbarlaststate = 1;
public Form1()
{
InitializeComponent();
_settings = AppSettingsLoader.Load();
_orchestrator = new DeploymentOrchestrator(_settings);
_sonicDiscovery = new SonicContainerDiscovery(_settings.SonicConnections);
BuildDesign();
Shown += async (_, _) => await LoadTargetsAsync();
}
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 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: Version
// -----------------------------
Panel footerPanel = new Panel
{
Dock = DockStyle.Bottom,
Height = 70,
Padding = new Padding(18, 8, 12, 8)
};
Panel footerSeparator = new Panel
{
Dock = DockStyle.Top,
Height = 1,
BackColor = BorderColor
};
Label versionLabel = new Label
{
Text = "INTERNAL TOOL",
ForeColor = MutedTextColor,
Font = new Font("Segoe UI", 7.5f, FontStyle.Bold),
AutoSize = true,
Location = new Point(18, 17)
};
Label versionNumber = new Label
{
Text = "Version 0.1.0",
ForeColor = BlueColor,
Font = new Font("Segoe UI", 8),
AutoSize = true,
Location = new Point(18, 37)
};
footerPanel.Controls.Add(footerSeparator);
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",
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)|*.cer;*.crt;*.pem;*.pfx|" +
"CER-Zertifikate (*.cer)|*.cer|" +
"CRT-Zertifikate (*.crt)|*.crt|" +
"PEM-Zertifikate (*.pem)|*.pem|" +
"PFX-Zertifikate (*.pfx)|*.pfx",
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 oder .pfx",
"Nicht unterstütztes Dateiformat",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
return;
}
CertificateInfo? certificateInfo = TryReadSelectedCertificate(selectedPath);
if (certificateInfo == null)
{
return;
}
txtCertificatePath.Text = selectedPath;
_loadedCertificateInfo = certificateInfo;
SetStatus($"Zertifikat ausgewählt: {Path.GetFileName(selectedPath)}", isError: false);
DisplayCertificateInfo(certificateInfo);
UpdateToNextStep(2);
SetFileButtonState(fileLoaded: true);
RefreshActionButtonStates();
}
private CertificateInfo? TryReadSelectedCertificate(string selectedPath)
{
bool isPfx = Path.GetExtension(selectedPath)
.Equals(".pfx", StringComparison.OrdinalIgnoreCase);
try
{
// Zuerst ohne Passwort versuchen
return ReadCertificate(selectedPath, null);
}
catch (CryptographicException) when (isPfx)
{
// Passwort nur abfragen, wenn das Laden ohne Passwort fehlgeschlagen ist
string? pfxPassword = PromptForPfxPassword();
if (pfxPassword == null)
{
return null;
}
try
{
return ReadCertificate(selectedPath, pfxPassword);
}
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;
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);
using X509Certificate2 certificate =
extension.Equals(".pfx", StringComparison.OrdinalIgnoreCase)
? 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"
};
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("Environment", "Umgebung");
dgvTargets.Columns.Add("Container", "Container / Service");
dgvTargets.Columns.Add("Tls", "TLS-Prüfung");
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();
btnRestartOnly = CreateSecondaryButton("ESB neu starten");
btnRestartOnly.Size = new Size(150, 42);
btnRestartOnly.Anchor = AnchorStyles.Top | AnchorStyles.Right;
btnRestartOnly.Click += async (_, _) => await RunRestartOnlyAsync();
btnDeploy = CreatePrimaryButton("Deployment starten");
btnDeploy.Size = new Size(170, 42);
btnDeploy.Anchor = AnchorStyles.Top | AnchorStyles.Right;
btnDeploy.Click += async (_, _) => await RunDeploymentAsync();
panel.Controls.Add(btnCancelRun);
panel.Controls.Add(btnValidate);
panel.Controls.Add(btnRestartOnly);
panel.Controls.Add(btnDeploy);
panel.Resize += (_, _) =>
{
btnDeploy.Left = panel.Width - btnDeploy.Width;
btnRestartOnly.Left = btnDeploy.Left - btnRestartOnly.Width - 12;
btnValidate.Left = btnRestartOnly.Left - btnValidate.Width - 12;
btnCancelRun.Left = btnValidate.Left - btnCancelRun.Width - 12;
};
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("Lade Bereitstellungsziele…", isError: false);
(ITargetRepository repository, string loadMessage) =
await TargetRepositoryFactory.CreateAsync(_settings);
_loadedTargets = await repository.GetActiveTargetsAsync();
BindTargetsToGrid(_loadedTargets);
SetStatus(loadMessage, isError: false);
}
catch (Exception ex)
{
_loadedTargets = [];
dgvTargets.Rows.Clear();
SetStatus($"Ziele konnten nicht geladen werden: {ex.Message}", isError: true);
}
finally
{
RefreshActionButtonStates();
}
}
private void BindTargetsToGrid(IReadOnlyList<DeploymentTarget> targets)
{
dgvTargets.Rows.Clear();
foreach (DeploymentTarget target in targets)
{
int tlsPort = target.TlsPort is > 0 ? target.TlsPort.Value : 443;
string tls = string.IsNullOrWhiteSpace(target.TlsHost)
? "—"
: $"{target.TlsHost}:{tlsPort}";
int rowIndex = dgvTargets.Rows.Add(
false,
target.Name,
target.Environment,
target.ContainerName,
tls,
"Bereit");
dgvTargets.Rows[rowIndex].Tag = target;
}
}
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;
btnValidate.Enabled = idle && hasCertificate && hasTargets;
btnDeploy.Enabled = idle && hasCertificate && hasSelection;
if (btnRestartOnly is not null)
{
btnRestartOnly.Enabled = idle && hasSelection;
}
if (dgvTargets is not null)
{
dgvTargets.Enabled = idle;
}
if (btnLoadFromSonic is not null)
{
btnLoadFromSonic.Enabled = idle && _sonicDiscovery.HasConnections;
}
}
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 (_isOperationRunning)
{
return Task.CompletedTask;
}
List<DeploymentTarget> selected = GetSelectedTargets();
PreflightValidationResult result = _orchestrator.ValidatePreflight(
txtCertificatePath.Text,
_loadedCertificateInfo,
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 (_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" +
"Laut Sonic/CX-Messenger Doku (Management Application API):\n" +
"IAgentProxy.restart dieselbe Aktion wie Restart in der SMC.\n" +
$"Verbindung: appsettings ConnectionUrl + User/Pass.\n" +
"Kein stopcontainer.bat nötig.\n\n" +
"Voraussetzung: Java + Client-JARs unter SonicHome\\lib (SMC-Installation).",
"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);
}
string details = string.Join(
Environment.NewLine + Environment.NewLine,
runResult.TargetResults.Select(r =>
$"{r.TargetName}: {r.StatusText}" +
(string.IsNullOrWhiteSpace(r.Detail) ? string.Empty : Environment.NewLine + r.Detail)));
SetStatus(
runResult.OverallSuccess
? $"Neustart verifiziert ({runResult.TargetResults.Count} Ziel(e))."
: "Neustart fehlgeschlagen oder nicht verifiziert.",
isError: !runResult.OverallSuccess);
MessageBox.Show(
this,
details,
runResult.OverallSuccess ? "Neustart verifiziert" : "Neustart fehlgeschlagen",
MessageBoxButtons.OK,
runResult.OverallSuccess ? MessageBoxIcon.Information : MessageBoxIcon.Warning);
}
catch (OperationCanceledException)
{
SetStatus("Neustart abgebrochen.", isError: true);
}
catch (Exception ex)
{
SetStatus($"Neustart fehlgeschlagen: {ex.Message}", isError: true);
MessageBox.Show(this, ex.Message, "ESB neu starten", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
SetOperationRunning(false);
_runCts?.Dispose();
_runCts = null;
}
}
private async Task RunDeploymentAsync()
{
if (_isOperationRunning || _loadedCertificateInfo is null)
{
return;
}
List<DeploymentTarget> selected = GetSelectedTargets();
PreflightValidationResult preflight = _orchestrator.ValidatePreflight(
txtCertificatePath.Text,
_loadedCertificateInfo,
selected);
if (!preflight.IsValid)
{
ShowPreflightIssues(
preflight,
"Deployment blockiert Vorabprüfung fehlgeschlagen.",
"Deployment");
return;
}
_runCts?.Dispose();
_runCts = new CancellationTokenSource();
SetOperationRunning(true);
UpdateToNextStep(3);
SetStatus($"Deployment 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.RunAsync(
txtCertificatePath.Text,
_loadedCertificateInfo,
selected,
progress,
_runCts.Token);
foreach (TargetStepResult targetResult in runResult.TargetResults)
{
SetTargetRowStatus(targetResult.TargetId, targetResult.StatusText);
}
UpdateToNextStep(4);
SetStatus(
runResult.OverallSuccess
? $"Deployment erfolgreich ({runResult.TargetResults.Count} Ziel(e))."
: "Deployment mit Fehlern beendet. Details in der Status-Spalte / Log.",
isError: !runResult.OverallSuccess);
}
catch (OperationCanceledException)
{
SetStatus("Deployment abgebrochen.", isError: true);
}
catch (Exception ex)
{
SetStatus($"Deployment fehlgeschlagen: {ex.Message}", isError: true);
MessageBox.Show(
this,
ex.Message,
"Deployment",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
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 Management Console Container-Discovery
// ---------------------------------------------------------------
private void BuildSonicDiscoveryRow(Panel card)
{
Label sonicLabel = new Label
{
Text = "Sonic Management:",
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)
};
foreach (SonicConnection conn in _sonicDiscovery.Connections)
{
cmbSonicConnection.Items.Add(conn.Name);
}
btnLoadFromSonic = CreateSecondaryButton("Container laden");
btnLoadFromSonic.Location = new Point(392, 103);
btnLoadFromSonic.Size = new Size(148, 28);
btnLoadFromSonic.Enabled = _sonicDiscovery.HasConnections;
btnLoadFromSonic.Click += async (_, _) => await LoadFromSonicAsync();
lblSonicStatus = new Label
{
ForeColor = MutedTextColor,
Font = new Font("Segoe UI", 8.5f),
AutoSize = true,
MaximumSize = new Size(720, 0),
Location = new Point(552, 100)
};
cmbSonicConnection.SelectedIndexChanged += (_, _) => RefreshSonicStatusLine();
if (cmbSonicConnection.Items.Count > 0)
{
cmbSonicConnection.SelectedIndex = 0;
}
else
{
RefreshSonicStatusLine();
}
card.Controls.Add(sonicLabel);
card.Controls.Add(cmbSonicConnection);
card.Controls.Add(btnLoadFromSonic);
card.Controls.Add(lblSonicStatus);
}
private void RefreshSonicStatusLine(string? suffix = null)
{
if (!_sonicDiscovery.HasConnections)
{
lblSonicStatus.Text = "Keine Sonic-Verbindung in appsettings konfiguriert";
lblSonicStatus.ForeColor = RedColor;
return;
}
string? selectedName = cmbSonicConnection?.SelectedItem?.ToString();
SonicConnection conn = _sonicDiscovery.Connections
.FirstOrDefault(c => string.Equals(c.Name, selectedName, StringComparison.OrdinalIgnoreCase))
?? _sonicDiscovery.Connections[0];
string sonicHome = string.IsNullOrWhiteSpace(conn.SonicHome) ? "(leer)" : conn.SonicHome.Trim();
(string home, string? javaExe) = new SonicMfApiExecutor(conn).ResolveRuntimePaths();
string javaDisplay = string.IsNullOrWhiteSpace(javaExe)
? "java=? (unter SonicHome/JRE setzen)"
: $"java={javaExe}";
string baseText =
$"{conn.ManagementModeDisplay} | SonicHome={home} | {javaDisplay}";
if (!string.IsNullOrWhiteSpace(suffix))
{
baseText = $"{suffix} | {baseText}";
}
lblSonicStatus.Text = baseText;
lblSonicStatus.ForeColor = string.IsNullOrWhiteSpace(javaExe) ? GoldColor : GreenColor;
}
private static string TruncateStatus(string? text, int max = 120)
{
if (string.IsNullOrWhiteSpace(text))
{
return "unbekannt";
}
string oneLine = text.Replace('\r', ' ').Replace('\n', ' ').Trim();
return oneLine.Length <= max ? oneLine : oneLine[..max] + "…";
}
private async Task LoadFromSonicAsync()
{
if (_isOperationRunning) return;
string? selectedConnection = cmbSonicConnection?.SelectedItem?.ToString();
if (string.IsNullOrWhiteSpace(selectedConnection)) return;
btnLoadFromSonic.Enabled = false;
lblSonicStatus.Text = $"Verbinde mit {selectedConnection}…";
lblSonicStatus.ForeColor = GoldColor;
try
{
using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30));
SonicDiscoveryResult result = await _sonicDiscovery.DiscoverAsync(
selectedConnection,
_loadedTargets,
cts.Token);
if (!result.Success)
{
RefreshSonicStatusLine($"Fehler: {TruncateStatus(result.ErrorMessage)}");
lblSonicStatus.ForeColor = RedColor;
SetStatus($"Sonic-Discovery fehlgeschlagen: {result.ErrorMessage}", isError: true);
return;
}
// Grid mit entdeckten + konfigurierten Containern befüllen
_loadedTargets = result.DiscoveredTargets;
BindTargetsToGrid(result.DiscoveredTargets);
int total = result.DiscoveredTargets.Count;
int remote = result.RawContainerNames.Count;
string domain = string.IsNullOrWhiteSpace(result.DomainName)
? selectedConnection
: result.DomainName;
if (total == 0)
{
RefreshSonicStatusLine($"Verb. ok 0 Container '{domain}'");
lblSonicStatus.ForeColor = GoldColor;
SetStatus(
result.ErrorMessage
?? $"Sonic Domain '{domain}': keine Container. KnownContainers/SonicHome in appsettings prüfen.",
isError: true);
}
else if (remote == 0)
{
RefreshSonicStatusLine($"Verb. ok 0 remote, {total} Config '{domain}'");
lblSonicStatus.ForeColor = GoldColor;
SetStatus(
$"Domain '{domain}': Remote leer {total} Ziel(e) aus appsettings/KnownContainers. "
+ (result.ErrorMessage ?? string.Empty),
isError: false);
}
else
{
RefreshSonicStatusLine($"Domain '{domain}': {remote} remote, {total} gesamt");
SetStatus($"Sonic Domain '{domain}': {total} Ziel(e) geladen ({remote} remote).", isError: false);
}
MarkUnconfiguredRows();
}
catch (OperationCanceledException)
{
RefreshSonicStatusLine("Timeout");
lblSonicStatus.ForeColor = RedColor;
SetStatus($"Sonic-Discovery Timeout ({selectedConnection})", isError: true);
}
catch (Exception ex)
{
RefreshSonicStatusLine($"Fehler: {TruncateStatus(ex.Message)}");
lblSonicStatus.ForeColor = RedColor;
SetStatus($"Sonic-Discovery Fehler: {ex.Message}", isError: true);
}
finally
{
btnLoadFromSonic.Enabled = _sonicDiscovery.HasConnections && !_isOperationRunning;
RefreshActionButtonStates();
}
}
/// <summary>
/// Färbt Zeilen mit unvollständiger Konfiguration (kein TargetDirectory) orange ein
/// damit erkennbar ist, dass diese Container noch konfiguriert werden müssen.
/// </summary>
private void MarkUnconfiguredRows()
{
foreach (DataGridViewRow row in dgvTargets.Rows)
{
if (row.Tag is not DeploymentTarget target) continue;
if (string.IsNullOrWhiteSpace(target.TargetDirectory))
{
row.DefaultCellStyle.ForeColor = GoldColor;
if (row.Cells["Status"] is DataGridViewCell statusCell)
{
statusCell.Value = "⚠ Pfad fehlt";
}
}
}
}
}
}