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 _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 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 GetSelectedTargets() { List 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 selected = GetSelectedTargets(); PreflightValidationResult result = _orchestrator.ValidatePreflight( txtCertificatePath.Text, _loadedCertificateInfo, selected); HashSet 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 selected = GetSelectedTargets(); PreflightValidationResult preflight = _orchestrator.ValidateRestartOnly(selected); if (!preflight.IsValid) { ShowPreflightIssues( preflight, "Neustart blockiert – Vorabprüfung fehlgeschlagen.", "ESB neu starten"); return; } DialogResult confirm = MessageBox.Show( this, $"ESB/Container für {selected.Count} Ziel(e) wirklich neu starten?\n\n" + "Es wird stopcontainer/startcontainer über die Sonic-Verbindung ausgeführt\n" + "(WinRM Remote-CMD oder LocalCmd laut appsettings).", "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 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 erfolgreich ({runResult.TargetResults.Count} Ziel(e))." : "Neustart mit Fehlern beendet. Details in Status-Spalte / Log.", isError: !runResult.OverallSuccess); } 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 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 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); } if (cmbSonicConnection.Items.Count > 0) { cmbSonicConnection.SelectedIndex = 0; } 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, Location = new Point(552, 109) }; if (!_sonicDiscovery.HasConnections) { lblSonicStatus.Text = "Keine Sonic-Verbindung in appsettings konfiguriert"; lblSonicStatus.ForeColor = RedColor; } else { SonicConnection first = _sonicDiscovery.Connections[0]; lblSonicStatus.Text = $"{_sonicDiscovery.Connections.Count} Verb. | {first.ManagementMode} | {first.DomainName} | SonicHome={first.SonicHome}"; lblSonicStatus.ForeColor = GreenColor; } card.Controls.Add(sonicLabel); card.Controls.Add(cmbSonicConnection); card.Controls.Add(btnLoadFromSonic); card.Controls.Add(lblSonicStatus); } 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) { lblSonicStatus.Text = $"Fehler: {result.ErrorMessage}"; lblSonicStatus.ForeColor = RedColor; SetStatus($"Sonic-Discovery fehlgeschlagen: {result.ErrorMessage}", isError: true); return; } // Grid mit entdeckten Containern befüllen _loadedTargets = result.DiscoveredTargets; BindTargetsToGrid(result.DiscoveredTargets); int configured = result.DiscoveredTargets.Count(t => !string.IsNullOrWhiteSpace(t.TargetDirectory)); int discovered = result.RawContainerNames.Count; lblSonicStatus.Text = discovered > 0 ? $"{discovered} Container gefunden, {configured} konfiguriert" : $"Verbindung ok – keine Container in Domain '{selectedConnection}'"; lblSonicStatus.ForeColor = discovered > 0 ? GreenColor : GoldColor; SetStatus( $"Sonic {selectedConnection}: {discovered} Container geladen.", isError: false); // Nicht-konfigurierte Ziele visuell markieren MarkUnconfiguredRows(); } catch (OperationCanceledException) { lblSonicStatus.Text = "Timeout – Verbindung nicht erreichbar"; lblSonicStatus.ForeColor = RedColor; SetStatus($"Sonic-Discovery Timeout ({selectedConnection})", isError: true); } catch (Exception ex) { lblSonicStatus.Text = $"Fehler: {ex.Message}"; lblSonicStatus.ForeColor = RedColor; SetStatus($"Sonic-Discovery Fehler: {ex.Message}", isError: true); } finally { btnLoadFromSonic.Enabled = _sonicDiscovery.HasConnections && !_isOperationRunning; RefreshActionButtonStates(); } } /// /// Färbt Zeilen mit unvollständiger Konfiguration (kein TargetDirectory) orange ein /// damit erkennbar ist, dass diese Container noch konfiguriert werden müssen. /// 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"; } } } } } }