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