Parse setenv scripts, search recursively under Sonic/MQ/ESB roots, and surface resolved SonicHome + java paths in the UI and full error lists. Co-authored-by: Cursor <cursoragent@cursor.com>
1958 lines
68 KiB
C#
1958 lines
68 KiB
C#
using System.Security.Cryptography;
|
||
using System.Security.Cryptography.X509Certificates;
|
||
using ZA.CoreService.ESBCertificateManager.Configuration;
|
||
using ZA.CoreService.ESBCertificateManager.Data;
|
||
using ZA.CoreService.ESBCertificateManager.Models;
|
||
using ZA.CoreService.ESBCertificateManager.Services;
|
||
|
||
namespace ZA.CoreService.ESBCertificateManager
|
||
{
|
||
public partial class Form1 : Form
|
||
{
|
||
// ---------------------------------------------------------
|
||
// ZIEHL-ABEGG-inspirierte Dark-Mode-Farbpalette
|
||
// ---------------------------------------------------------
|
||
|
||
// Große Hintergrundflächen
|
||
private readonly Color BackgroundColor = Color.FromArgb(10, 18, 34); // #0A1222
|
||
private readonly Color SidebarColor = Color.FromArgb(7, 23, 45); // #07172D
|
||
private readonly Color CardColor = Color.FromArgb(16, 38, 65); // #102641
|
||
private readonly Color CardHoverColor = Color.FromArgb(23, 55, 94); // #17375E
|
||
private readonly Color BorderColor = Color.FromArgb(36, 74, 117); // #244A75
|
||
|
||
// ZIEHL-ABEGG-nahe Markenakzente
|
||
private readonly Color BlueColor = Color.FromArgb(0, 110, 182); // #006EB6
|
||
private readonly Color BrightBlueColor = Color.FromArgb(0, 139, 210); // #008BD2
|
||
private readonly Color GoldColor = Color.FromArgb(208, 171, 57); // #D0AB39
|
||
private readonly Color LightGoldColor = Color.FromArgb(226, 196, 93); // #E2C45D
|
||
|
||
// Schrift und Statusfarben
|
||
private readonly Color TextColor = Color.FromArgb(241, 245, 249); // #F1F5F9
|
||
private readonly Color MutedTextColor = Color.FromArgb(169, 184, 200); // #A9B8C8
|
||
private readonly Color GreenColor = Color.FromArgb(60, 203, 127); // #3CCB7F
|
||
private readonly Color RedColor = Color.FromArgb(239, 106, 106); // #EF6A6A
|
||
|
||
private readonly AppSettings _settings;
|
||
private readonly DeploymentOrchestrator _orchestrator;
|
||
private readonly SonicContainerDiscovery _sonicDiscovery;
|
||
|
||
private Label lblStatus = null!;
|
||
private Label lblStatusDot = null!;
|
||
private TextBox txtCertificatePath = null!;
|
||
private DataGridView dgvTargets = null!;
|
||
private ComboBox cmbSonicConnection = null!;
|
||
private Button btnLoadFromSonic = null!;
|
||
private Label lblSonicStatus = null!;
|
||
|
||
private Panel navProgressPanel = null!;
|
||
|
||
private Panel titleBar = null!;
|
||
private Button btnMinimize = null!;
|
||
private Button btnMaximize = null!;
|
||
private Button btnClose = null!;
|
||
private Button btnValidate = null!;
|
||
private Button btnDeploy = null!;
|
||
private Button btnRestartOnly = null!;
|
||
private Button btnCancelRun = null!;
|
||
|
||
private bool isMaximized = false;
|
||
private Point dragStartPoint;
|
||
|
||
private Label lblCertificateSubject = null!;
|
||
private Label lblCertificateIssuer = null!;
|
||
private Label lblCertificateExpiry = null!;
|
||
private Label lblCertificateFingerprint = null!;
|
||
private Label lblCertificateValidity = null!;
|
||
|
||
private Button btnSelectFile = null!;
|
||
private bool isCertificateFileLoaded;
|
||
private CertificateInfo? _loadedCertificateInfo;
|
||
private IReadOnlyList<DeploymentTarget> _loadedTargets = [];
|
||
private bool _isOperationRunning;
|
||
private CancellationTokenSource? _runCts;
|
||
private int nvbarlaststate = 1;
|
||
|
||
public Form1()
|
||
{
|
||
InitializeComponent();
|
||
|
||
_settings = AppSettingsLoader.Load();
|
||
_orchestrator = new DeploymentOrchestrator(_settings);
|
||
_sonicDiscovery = new SonicContainerDiscovery(_settings.SonicConnections);
|
||
|
||
BuildDesign();
|
||
Shown += async (_, _) => await LoadTargetsAsync();
|
||
}
|
||
private Button CreateWindowButton(string text)
|
||
{
|
||
return new Button
|
||
{
|
||
Text = text,
|
||
Width = 46,
|
||
Height = 34,
|
||
FlatStyle = FlatStyle.Flat,
|
||
FlatAppearance =
|
||
{
|
||
BorderSize = 0,
|
||
MouseOverBackColor = CardHoverColor
|
||
},
|
||
BackColor = SidebarColor,
|
||
ForeColor = MutedTextColor,
|
||
Font = new Font("Segoe UI", 11),
|
||
Cursor = Cursors.Hand,
|
||
TabStop = false
|
||
};
|
||
}
|
||
|
||
private void ToggleMaximizeWindow()
|
||
{
|
||
if (isMaximized)
|
||
{
|
||
WindowState = FormWindowState.Normal;
|
||
isMaximized = false;
|
||
}
|
||
else
|
||
{
|
||
WindowState = FormWindowState.Maximized;
|
||
isMaximized = true;
|
||
}
|
||
}
|
||
|
||
private void TitleBar_MouseDown(object? sender, MouseEventArgs e)
|
||
{
|
||
if (e.Button == MouseButtons.Left)
|
||
{
|
||
dragStartPoint = e.Location;
|
||
}
|
||
}
|
||
|
||
private void TitleBar_MouseMove(object? sender, MouseEventArgs e)
|
||
{
|
||
if (e.Button == MouseButtons.Left)
|
||
{
|
||
Left += e.X - dragStartPoint.X;
|
||
Top += e.Y - dragStartPoint.Y;
|
||
}
|
||
}
|
||
private Panel BuildTitleBar()
|
||
{
|
||
Panel panel = new Panel
|
||
{
|
||
Height = 34,
|
||
BackColor = SidebarColor
|
||
};
|
||
|
||
Label appIcon = new Label
|
||
{
|
||
Text = "◆",
|
||
ForeColor = GoldColor,
|
||
Font = new Font("Segoe UI Symbol", 12, FontStyle.Bold),
|
||
AutoSize = true,
|
||
Location = new Point(12, 8)
|
||
};
|
||
|
||
Label appName = new Label
|
||
{
|
||
Text = "ESB Certificate Manager",
|
||
ForeColor = MutedTextColor,
|
||
Font = new Font("Segoe UI", 9),
|
||
AutoSize = true,
|
||
Location = new Point(31, 9)
|
||
};
|
||
|
||
btnClose = CreateWindowButton("×");
|
||
btnMaximize = CreateWindowButton("□");
|
||
btnMinimize = CreateWindowButton("−");
|
||
|
||
btnClose.ForeColor = TextColor;
|
||
btnClose.FlatAppearance.MouseOverBackColor = Color.FromArgb(190, 35, 45);
|
||
|
||
btnClose.Click += (_, _) => Close();
|
||
|
||
btnMinimize.Click += (_, _) =>
|
||
{
|
||
WindowState = FormWindowState.Minimized;
|
||
};
|
||
|
||
btnMaximize.Click += (_, _) =>
|
||
{
|
||
ToggleMaximizeWindow();
|
||
};
|
||
|
||
panel.Resize += (_, _) =>
|
||
{
|
||
btnClose.Left = panel.Width - btnClose.Width;
|
||
btnMaximize.Left = btnClose.Left - btnMaximize.Width;
|
||
btnMinimize.Left = btnMaximize.Left - btnMinimize.Width;
|
||
};
|
||
|
||
panel.MouseDown += TitleBar_MouseDown;
|
||
panel.MouseMove += TitleBar_MouseMove;
|
||
appIcon.MouseDown += TitleBar_MouseDown;
|
||
appIcon.MouseMove += TitleBar_MouseMove;
|
||
appName.MouseDown += TitleBar_MouseDown;
|
||
appName.MouseMove += TitleBar_MouseMove;
|
||
|
||
panel.Controls.Add(appIcon);
|
||
panel.Controls.Add(appName);
|
||
panel.Controls.Add(btnMinimize);
|
||
panel.Controls.Add(btnMaximize);
|
||
panel.Controls.Add(btnClose);
|
||
|
||
return panel;
|
||
}
|
||
private void BuildDesign()
|
||
{
|
||
Text = "ESB Certificate Manager";
|
||
StartPosition = FormStartPosition.CenterScreen;
|
||
|
||
MinimumSize = new Size(1100, 700);
|
||
Size = new Size(1400, 850);
|
||
|
||
BackColor = BackgroundColor;
|
||
Font = new Font("Segoe UI", 10);
|
||
ForeColor = TextColor;
|
||
|
||
// Entfernt die weiße Windows-Standardtitelleiste.
|
||
FormBorderStyle = FormBorderStyle.None;
|
||
|
||
// Eigene dunkle Titelleiste.
|
||
titleBar = BuildTitleBar();
|
||
titleBar.Dock = DockStyle.Top;
|
||
Controls.Add(titleBar);
|
||
|
||
Panel sidebar = new Panel
|
||
{
|
||
Dock = DockStyle.Left,
|
||
Width = 205,
|
||
BackColor = SidebarColor
|
||
};
|
||
|
||
Panel content = new Panel
|
||
{
|
||
Dock = DockStyle.Fill,
|
||
BackColor = BackgroundColor,
|
||
Padding = new Padding(38, 48, 38, 34),
|
||
AutoScroll = true
|
||
};
|
||
|
||
// Wichtig für Docking:
|
||
// Erst der Fill-Bereich, dann die feste Sidebar.
|
||
Controls.Add(content);
|
||
Controls.Add(sidebar);
|
||
|
||
// Dezente vertikale Linie rechts an der Sidebar.
|
||
Panel sidebarBorder = new Panel
|
||
{
|
||
Dock = DockStyle.Right,
|
||
Width = 1,
|
||
BackColor = BorderColor
|
||
};
|
||
|
||
sidebar.Controls.Add(sidebarBorder);
|
||
|
||
BuildSidebar(sidebar);
|
||
BuildContent(content);
|
||
}
|
||
|
||
private void BuildSidebar(Panel sidebar)
|
||
{
|
||
|
||
// -------------------------
|
||
// 1. Logo / Produktbereich
|
||
// -------------------------
|
||
Panel logoPanel = new Panel
|
||
{
|
||
Dock = DockStyle.Top,
|
||
Height = 165,
|
||
Padding = new Padding(18, 18, 18, 8)
|
||
};
|
||
|
||
PictureBox pictureLogo = new PictureBox
|
||
{
|
||
Location = new Point(18, 14),
|
||
Size = new Size(165, 58),
|
||
SizeMode = PictureBoxSizeMode.Zoom,
|
||
BackColor = Color.Transparent
|
||
};
|
||
|
||
string logoPath = Path.Combine(
|
||
Application.StartupPath,
|
||
"Assets",
|
||
"logo.png");
|
||
|
||
if (File.Exists(logoPath))
|
||
{
|
||
pictureLogo.Image = Image.FromFile(logoPath);
|
||
}
|
||
|
||
Label logoSubtitle = new Label
|
||
{
|
||
Text = "ESB CERTIFICATE MANAGER",
|
||
ForeColor = TextColor,
|
||
Font = new Font("Segoe UI", 8, FontStyle.Bold),
|
||
AutoSize = true,
|
||
Location = new Point(18, 88)
|
||
};
|
||
|
||
|
||
Panel logoSeparator = new Panel
|
||
{
|
||
BackColor = BorderColor,
|
||
Location = new Point(22, 138),
|
||
Size = new Size(168, 1)
|
||
};
|
||
|
||
logoPanel.Controls.Add(pictureLogo);
|
||
logoPanel.Controls.Add(logoSubtitle);
|
||
logoPanel.Controls.Add(logoSeparator);
|
||
|
||
// ------------------------------------
|
||
// 2. Linker Ablauf: keine Navigation,
|
||
// sondern Fortschritt im Deployment
|
||
// ------------------------------------
|
||
Panel progressPanel = new Panel
|
||
{
|
||
Dock = DockStyle.Top,
|
||
Height = 360,
|
||
Padding = new Padding(18, 8, 18, 8)
|
||
};
|
||
navProgressPanel = progressPanel;
|
||
|
||
Label progressTitle = new Label
|
||
{
|
||
Text = "DEPLOYMENT-ABLAUF",
|
||
ForeColor = MutedTextColor,
|
||
Font = new Font("Segoe UI", 7.5f, FontStyle.Bold),
|
||
AutoSize = true,
|
||
Location = new Point(18, 12)
|
||
};
|
||
|
||
AddStep(
|
||
progressPanel,
|
||
y: 48,
|
||
number: "1",
|
||
title: "Zertifikat",
|
||
description: "Datei auswählen",
|
||
isActive: true,
|
||
hasNextStep: true);
|
||
|
||
AddStep(
|
||
progressPanel,
|
||
y: 116,
|
||
number: "2",
|
||
title: "Ziele",
|
||
description: "Systeme auswählen",
|
||
isActive: false,
|
||
hasNextStep: true);
|
||
|
||
AddStep(
|
||
progressPanel,
|
||
y: 184,
|
||
number: "3",
|
||
title: "Bereitstellen",
|
||
description: "Kopieren & Neustart",
|
||
isActive: false,
|
||
hasNextStep: true);
|
||
|
||
AddStep(
|
||
progressPanel,
|
||
y: 252,
|
||
number: "4",
|
||
title: "TLS-Prüfung",
|
||
description: "Zertifikat bestätigen",
|
||
isActive: false,
|
||
hasNextStep: false);
|
||
|
||
progressPanel.Controls.Add(progressTitle);
|
||
|
||
// -----------------------------
|
||
// 3. Footer unten: Version
|
||
// -----------------------------
|
||
Panel footerPanel = new Panel
|
||
{
|
||
Dock = DockStyle.Bottom,
|
||
Height = 70,
|
||
Padding = new Padding(18, 8, 12, 8)
|
||
};
|
||
|
||
Panel footerSeparator = new Panel
|
||
{
|
||
Dock = DockStyle.Top,
|
||
Height = 1,
|
||
BackColor = BorderColor
|
||
};
|
||
|
||
Label versionLabel = new Label
|
||
{
|
||
Text = "INTERNAL TOOL",
|
||
ForeColor = MutedTextColor,
|
||
Font = new Font("Segoe UI", 7.5f, FontStyle.Bold),
|
||
AutoSize = true,
|
||
Location = new Point(18, 17)
|
||
};
|
||
|
||
Label versionNumber = new Label
|
||
{
|
||
Text = "Version 0.1.0",
|
||
ForeColor = BlueColor,
|
||
Font = new Font("Segoe UI", 8),
|
||
AutoSize = true,
|
||
Location = new Point(18, 37)
|
||
};
|
||
|
||
footerPanel.Controls.Add(footerSeparator);
|
||
footerPanel.Controls.Add(versionLabel);
|
||
footerPanel.Controls.Add(versionNumber);
|
||
|
||
// Dock-Reihenfolge: Bottom, Top, Top
|
||
sidebar.Controls.Add(footerPanel);
|
||
sidebar.Controls.Add(progressPanel);
|
||
sidebar.Controls.Add(logoPanel);
|
||
}
|
||
|
||
private void AddStep(
|
||
Panel parent,
|
||
int y,
|
||
string number,
|
||
string title,
|
||
string description,
|
||
bool isActive,
|
||
bool hasNextStep)
|
||
{
|
||
Color stepColor = isActive ? GoldColor : MutedTextColor;
|
||
Color titleColor = isActive ? TextColor : MutedTextColor;
|
||
|
||
Label circle = new Label
|
||
{
|
||
Name = $"stepCircle{number}",
|
||
Text = number,
|
||
Location = new Point(18, y),
|
||
Size = new Size(25, 25),
|
||
TextAlign = ContentAlignment.MiddleCenter,
|
||
BackColor = isActive ? GoldColor : Color.FromArgb(25, 48, 76),
|
||
ForeColor = isActive ? SidebarColor : MutedTextColor,
|
||
Font = new Font("Segoe UI", 8, FontStyle.Bold)
|
||
};
|
||
|
||
Label titleLabel = new Label
|
||
{
|
||
Name = $"stepTitle{number}",
|
||
Text = title,
|
||
ForeColor = titleColor,
|
||
Font = new Font("Segoe UI", 9.5f, FontStyle.Bold),
|
||
AutoSize = true,
|
||
Location = new Point(56, y - 1)
|
||
};
|
||
|
||
Label descriptionLabel = new Label
|
||
{
|
||
Name = $"stepDescription{number}",
|
||
Text = description,
|
||
ForeColor = MutedTextColor,
|
||
Font = new Font("Segoe UI", 8),
|
||
AutoSize = true,
|
||
Location = new Point(56, y + 16)
|
||
};
|
||
|
||
parent.Controls.Add(circle);
|
||
parent.Controls.Add(titleLabel);
|
||
parent.Controls.Add(descriptionLabel);
|
||
|
||
if (hasNextStep)
|
||
{
|
||
Panel line = new Panel
|
||
{
|
||
BackColor = BorderColor,
|
||
Location = new Point(30, y + 26),
|
||
Size = new Size(1, 40)
|
||
};
|
||
|
||
parent.Controls.Add(line);
|
||
line.SendToBack();
|
||
}
|
||
}
|
||
private void UpdateToNextStep(int state)
|
||
{
|
||
|
||
Control[] foundCircles = navProgressPanel.Controls.Find($"stepCircle{state}", true);
|
||
if (foundCircles.Length == 0) return;
|
||
Control[] foundTitels = navProgressPanel.Controls.Find($"stepTitle{state}", true);
|
||
if (foundTitels.Length == 0) return;
|
||
Label titel = (Label)foundTitels[0];
|
||
Label circle = (Label)foundCircles[0];
|
||
|
||
|
||
Control[] foundlastTitels = navProgressPanel.Controls.Find($"stepTitle{nvbarlaststate}", true);
|
||
if (foundlastTitels.Length == 0) return;
|
||
Label lasttitel = (Label)foundlastTitels[0];
|
||
if(nvbarlaststate > state)
|
||
{
|
||
Control[] foundlastCircles = navProgressPanel.Controls.Find($"stepCircle{nvbarlaststate}", true);
|
||
if (foundlastCircles.Length == 0) return;
|
||
Label lastcircle = (Label)foundlastCircles[0];
|
||
lastcircle.BackColor = Color.FromArgb(25, 48, 76);
|
||
lastcircle.ForeColor = MutedTextColor;
|
||
lasttitel.ForeColor = MutedTextColor;
|
||
}
|
||
else
|
||
{
|
||
lasttitel.ForeColor = MutedTextColor;
|
||
}
|
||
|
||
|
||
circle.BackColor = GoldColor;
|
||
circle.ForeColor = SidebarColor;
|
||
titel.ForeColor = TextColor;
|
||
nvbarlaststate = state;
|
||
}
|
||
|
||
private Button CreateNavButton(string text, bool isActive)
|
||
{
|
||
Button button = new Button
|
||
{
|
||
Text = text,
|
||
Height = 43,
|
||
Dock = DockStyle.Top,
|
||
FlatStyle = FlatStyle.Flat,
|
||
FlatAppearance =
|
||
{
|
||
BorderSize = 0,
|
||
MouseOverBackColor = Color.FromArgb(37, 52, 74)
|
||
},
|
||
BackColor = isActive ? Color.FromArgb(35, 58, 92) : SidebarColor,
|
||
ForeColor = isActive ? TextColor : MutedTextColor,
|
||
Font = new Font("Segoe UI", 10, isActive ? FontStyle.Bold : FontStyle.Regular),
|
||
TextAlign = ContentAlignment.MiddleLeft,
|
||
Padding = new Padding(14, 0, 0, 0),
|
||
Cursor = Cursors.Hand
|
||
};
|
||
|
||
button.Click += (_, _) =>
|
||
{
|
||
SetStatus($"Navigation gewählt: {text.Trim()}", isError: false);
|
||
};
|
||
|
||
return button;
|
||
}
|
||
|
||
private void BuildContent(Panel content)
|
||
{
|
||
// Status unten zuerst hinzufügen, weil Dock-Reihenfolge relevant ist
|
||
Panel statusBar = BuildStatusBar();
|
||
statusBar.Dock = DockStyle.Bottom;
|
||
content.Controls.Add(statusBar);
|
||
|
||
Panel actionPanel = BuildActionPanel();
|
||
actionPanel.Dock = DockStyle.Bottom;
|
||
actionPanel.Height = 65;
|
||
content.Controls.Add(actionPanel);
|
||
|
||
Panel targetsCard = BuildTargetsCard();
|
||
targetsCard.Dock = DockStyle.Top;
|
||
targetsCard.Height = 380;
|
||
targetsCard.Margin = new Padding(0, 18, 0, 0);
|
||
content.Controls.Add(targetsCard);
|
||
|
||
Panel topCards = BuildTopCards();
|
||
topCards.Dock = DockStyle.Top;
|
||
topCards.Height = 225;
|
||
topCards.Margin = new Padding(0, 20, 0, 0);
|
||
content.Controls.Add(topCards);
|
||
|
||
Panel header = BuildHeader();
|
||
header.Dock = DockStyle.Top;
|
||
header.Height = 87;
|
||
content.Controls.Add(header);
|
||
}
|
||
|
||
private Panel BuildHeader()
|
||
{
|
||
Panel header = new Panel
|
||
{
|
||
BackColor = BackgroundColor
|
||
};
|
||
|
||
Label title = new Label
|
||
{
|
||
Text = "Zertifikat bereitstellen",
|
||
ForeColor = TextColor,
|
||
Font = new Font("Segoe UI", 23, FontStyle.Bold),
|
||
AutoSize = true,
|
||
Location = new Point(0, 0)
|
||
};
|
||
|
||
Label subtitle = new Label
|
||
{
|
||
Text = "Verteile ein Zertifikat auf konfigurierte Ziele und prüfe anschließend die TLS-Verbindung.",
|
||
ForeColor = MutedTextColor,
|
||
Font = new Font("Segoe UI", 10),
|
||
AutoSize = true,
|
||
Location = new Point(3, 43)
|
||
};
|
||
|
||
Label environmentBadge = new Label
|
||
{
|
||
Text = " UMGEBUNG ",
|
||
ForeColor = GoldColor,
|
||
BackColor = Color.FromArgb(65, 53, 17),
|
||
Font = new Font("Segoe UI", 8, FontStyle.Bold),
|
||
AutoSize = true,
|
||
Padding = new Padding(7, 6, 7, 6),
|
||
Anchor = AnchorStyles.Top | AnchorStyles.Right
|
||
};
|
||
|
||
environmentBadge.Location = new Point(900, 9);
|
||
|
||
header.Resize += (_, _) =>
|
||
{
|
||
environmentBadge.Left = header.Width - environmentBadge.Width;
|
||
};
|
||
|
||
header.Controls.Add(title);
|
||
header.Controls.Add(subtitle);
|
||
header.Controls.Add(environmentBadge);
|
||
|
||
return header;
|
||
}
|
||
|
||
private Panel BuildTopCards()
|
||
{
|
||
Panel container = new Panel
|
||
{
|
||
BackColor = BackgroundColor
|
||
};
|
||
|
||
Panel sourceCard = CreateCard();
|
||
sourceCard.Dock = DockStyle.Left;
|
||
sourceCard.Width = 520;
|
||
|
||
Panel detailsCard = CreateCard();
|
||
detailsCard.Dock = DockStyle.Fill;
|
||
detailsCard.Margin = new Padding(18, 0, 0, 0);
|
||
|
||
container.Controls.Add(detailsCard);
|
||
container.Controls.Add(sourceCard);
|
||
|
||
BuildSourceCard(sourceCard);
|
||
BuildDetailsCard(detailsCard);
|
||
|
||
return container;
|
||
}
|
||
private void BuildSourceCard(Panel card)
|
||
{
|
||
Label step = CreateSmallTitle("SCHRITT 1");
|
||
step.Location = new Point(22, 18);
|
||
|
||
Label title = CreateCardTitle("Zertifikatsquelle");
|
||
title.Location = new Point(22, 40);
|
||
|
||
Label description = CreateMutedLabel(
|
||
"Wähle die Zertifikatsdatei aus der zentralen Ablage.");
|
||
description.Location = new Point(22, 73);
|
||
|
||
txtCertificatePath = new TextBox
|
||
{
|
||
Location = new Point(22, 112),
|
||
Size = new Size(320, 38),
|
||
BackColor = Color.FromArgb(20, 30, 48),
|
||
ForeColor = TextColor,
|
||
BorderStyle = BorderStyle.FixedSingle,
|
||
Font = new Font("Segoe UI", 9),
|
||
Text = string.Empty,
|
||
PlaceholderText = "Keine Datei ausgewählt",
|
||
ReadOnly = true
|
||
};
|
||
|
||
btnSelectFile = CreateSecondaryButton("Datei auswählen");
|
||
btnSelectFile.Location = new Point(354, 111);
|
||
btnSelectFile.Size = new Size(140, 39);
|
||
SetFileButtonState(fileLoaded: false);
|
||
|
||
btnSelectFile.Click += (_, _) =>
|
||
{
|
||
if (isCertificateFileLoaded)
|
||
{
|
||
ClearSelectedCertificate();
|
||
return;
|
||
}
|
||
|
||
TrySelectCertificateFile();
|
||
};
|
||
|
||
Label hint = new Label
|
||
{
|
||
Text = "Unterstützte Formate: .cer, .crt, .pem, .pfx",
|
||
ForeColor = MutedTextColor,
|
||
Font = new Font("Segoe UI", 8.5f),
|
||
AutoSize = true,
|
||
Location = new Point(22, 164)
|
||
};
|
||
|
||
card.Controls.Add(step);
|
||
card.Controls.Add(title);
|
||
card.Controls.Add(description);
|
||
card.Controls.Add(txtCertificatePath);
|
||
card.Controls.Add(btnSelectFile);
|
||
card.Controls.Add(hint);
|
||
}
|
||
private void TrySelectCertificateFile()
|
||
{
|
||
using OpenFileDialog dialog = new OpenFileDialog
|
||
{
|
||
Title = "Zertifikatsdatei auswählen",
|
||
Filter =
|
||
"Unterstützte Zertifikatsdateien (*.cer;*.crt;*.pem;*.pfx)|*.cer;*.crt;*.pem;*.pfx|" +
|
||
"CER-Zertifikate (*.cer)|*.cer|" +
|
||
"CRT-Zertifikate (*.crt)|*.crt|" +
|
||
"PEM-Zertifikate (*.pem)|*.pem|" +
|
||
"PFX-Zertifikate (*.pfx)|*.pfx",
|
||
FilterIndex = 1,
|
||
Multiselect = false,
|
||
CheckFileExists = true,
|
||
CheckPathExists = true,
|
||
RestoreDirectory = true,
|
||
AddExtension = true,
|
||
DereferenceLinks = true
|
||
};
|
||
|
||
// Bei Abbruch bleibt der bisherige Zustand unverändert.
|
||
if (dialog.ShowDialog(this) != DialogResult.OK)
|
||
{
|
||
return;
|
||
}
|
||
|
||
string selectedPath = dialog.FileName;
|
||
|
||
if (!IsSupportedCertificateFile(selectedPath))
|
||
{
|
||
MessageBox.Show(
|
||
this,
|
||
"Bitte wähle eine Zertifikatsdatei in einem der folgenden Formate aus:\n" +
|
||
".cer, .crt, .pem oder .pfx",
|
||
"Nicht unterstütztes Dateiformat",
|
||
MessageBoxButtons.OK,
|
||
MessageBoxIcon.Warning);
|
||
return;
|
||
}
|
||
|
||
CertificateInfo? certificateInfo = TryReadSelectedCertificate(selectedPath);
|
||
if (certificateInfo == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
txtCertificatePath.Text = selectedPath;
|
||
_loadedCertificateInfo = certificateInfo;
|
||
SetStatus($"Zertifikat ausgewählt: {Path.GetFileName(selectedPath)}", isError: false);
|
||
DisplayCertificateInfo(certificateInfo);
|
||
UpdateToNextStep(2);
|
||
SetFileButtonState(fileLoaded: true);
|
||
RefreshActionButtonStates();
|
||
}
|
||
|
||
private CertificateInfo? TryReadSelectedCertificate(string selectedPath)
|
||
{
|
||
bool isPfx = Path.GetExtension(selectedPath)
|
||
.Equals(".pfx", StringComparison.OrdinalIgnoreCase);
|
||
|
||
try
|
||
{
|
||
// Zuerst ohne Passwort versuchen
|
||
return ReadCertificate(selectedPath, null);
|
||
}
|
||
catch (CryptographicException) when (isPfx)
|
||
{
|
||
// Passwort nur abfragen, wenn das Laden ohne Passwort fehlgeschlagen ist
|
||
string? pfxPassword = PromptForPfxPassword();
|
||
if (pfxPassword == null)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
try
|
||
{
|
||
return ReadCertificate(selectedPath, pfxPassword);
|
||
}
|
||
catch (CryptographicException)
|
||
{
|
||
MessageBox.Show(
|
||
"Das angegebene Passwort ist falsch oder die Datei ist beschädigt.",
|
||
"Fehler beim Lesen des Zertifikats",
|
||
MessageBoxButtons.OK,
|
||
MessageBoxIcon.Error);
|
||
return null;
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show(
|
||
this,
|
||
$"Das Zertifikat konnte nicht gelesen werden.\n\n{ex.Message}",
|
||
"Fehler beim Lesen des Zertifikats",
|
||
MessageBoxButtons.OK,
|
||
MessageBoxIcon.Error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
private void ClearSelectedCertificate()
|
||
{
|
||
txtCertificatePath.Text = string.Empty;
|
||
_loadedCertificateInfo = null;
|
||
SetStatus("Keine Datei ausgewählt", isError: false);
|
||
|
||
lblCertificateSubject.Text = "-";
|
||
lblCertificateIssuer.Text = "-";
|
||
lblCertificateExpiry.Text = "-";
|
||
lblCertificateFingerprint.Text = "-";
|
||
SetValidityBadge(
|
||
"○ NICHT GELADEN",
|
||
MutedTextColor,
|
||
Color.FromArgb(39, 52, 73));
|
||
|
||
UpdateToNextStep(1);
|
||
SetFileButtonState(fileLoaded: false);
|
||
RefreshActionButtonStates();
|
||
}
|
||
|
||
private void SetFileButtonState(bool fileLoaded)
|
||
{
|
||
isCertificateFileLoaded = fileLoaded;
|
||
|
||
if (fileLoaded)
|
||
{
|
||
btnSelectFile.Text = "Datei auswerfen";
|
||
btnSelectFile.BackColor = Color.FromArgb(72, 31, 38);
|
||
btnSelectFile.ForeColor = Color.FromArgb(255, 210, 210);
|
||
btnSelectFile.FlatAppearance.BorderColor = RedColor;
|
||
btnSelectFile.FlatAppearance.MouseOverBackColor = Color.FromArgb(96, 42, 50);
|
||
return;
|
||
}
|
||
|
||
btnSelectFile.Text = "Datei auswählen";
|
||
btnSelectFile.BackColor = Color.FromArgb(39, 52, 73);
|
||
btnSelectFile.ForeColor = TextColor;
|
||
btnSelectFile.FlatAppearance.BorderColor = BorderColor;
|
||
btnSelectFile.FlatAppearance.MouseOverBackColor = Color.FromArgb(54, 69, 94);
|
||
}
|
||
|
||
private void SetValidityBadge(string text, Color foreColor, Color backColor)
|
||
{
|
||
lblCertificateValidity.Text = text;
|
||
lblCertificateValidity.ForeColor = foreColor;
|
||
lblCertificateValidity.BackColor = backColor;
|
||
RepositionValidityBadge();
|
||
}
|
||
|
||
private void RepositionValidityBadge()
|
||
{
|
||
if (lblCertificateValidity?.Parent == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
lblCertificateValidity.Left =
|
||
lblCertificateValidity.Parent.Width - lblCertificateValidity.Width - 22;
|
||
}
|
||
|
||
private static string? PromptForPfxPassword()
|
||
{
|
||
using Form dialog = new Form
|
||
{
|
||
Text = "PFX-Passwort eingeben",
|
||
Size = new Size(360, 150),
|
||
StartPosition = FormStartPosition.CenterParent,
|
||
FormBorderStyle = FormBorderStyle.FixedDialog,
|
||
MinimizeBox = false,
|
||
MaximizeBox = false
|
||
};
|
||
|
||
Label label = new Label
|
||
{
|
||
Text = "Passwort für die PFX-Datei:",
|
||
Location = new Point(12, 15),
|
||
AutoSize = true
|
||
};
|
||
|
||
TextBox txtPwd = new TextBox
|
||
{
|
||
Location = new Point(12, 38),
|
||
Width = 320,
|
||
UseSystemPasswordChar = true
|
||
};
|
||
|
||
Button btnOk = new Button
|
||
{
|
||
Text = "OK",
|
||
DialogResult = DialogResult.OK,
|
||
Location = new Point(176, 72),
|
||
Width = 75
|
||
};
|
||
|
||
Button btnCancel = new Button
|
||
{
|
||
Text = "Abbrechen",
|
||
DialogResult = DialogResult.Cancel,
|
||
Location = new Point(257, 72),
|
||
Width = 75
|
||
};
|
||
|
||
dialog.Controls.AddRange([label, txtPwd, btnOk, btnCancel]);
|
||
dialog.AcceptButton = btnOk;
|
||
dialog.CancelButton = btnCancel;
|
||
|
||
return dialog.ShowDialog() == DialogResult.OK ? txtPwd.Text : null;
|
||
}
|
||
|
||
private static CertificateInfo ReadCertificate(string certificatePath, string? pfxPassword = null)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(certificatePath))
|
||
{
|
||
throw new ArgumentException(
|
||
"Es wurde kein Zertifikatspfad angegeben.",
|
||
nameof(certificatePath));
|
||
}
|
||
|
||
if (!File.Exists(certificatePath))
|
||
{
|
||
throw new FileNotFoundException(
|
||
"Die ausgewählte Zertifikatsdatei wurde nicht gefunden.",
|
||
certificatePath);
|
||
}
|
||
|
||
string extension = Path.GetExtension(certificatePath);
|
||
|
||
using X509Certificate2 certificate =
|
||
extension.Equals(".pfx", StringComparison.OrdinalIgnoreCase)
|
||
? new X509Certificate2(
|
||
certificatePath,
|
||
pfxPassword,
|
||
X509KeyStorageFlags.EphemeralKeySet)
|
||
: new X509Certificate2(certificatePath);
|
||
|
||
string subject = certificate.GetNameInfo(
|
||
X509NameType.SimpleName,
|
||
forIssuer: false);
|
||
|
||
string issuer = certificate.GetNameInfo(
|
||
X509NameType.SimpleName,
|
||
forIssuer: true);
|
||
|
||
if (string.IsNullOrWhiteSpace(subject))
|
||
{
|
||
subject = certificate.Subject;
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(issuer))
|
||
{
|
||
issuer = certificate.Issuer;
|
||
}
|
||
|
||
string fingerprint = certificate.GetCertHashString(
|
||
HashAlgorithmName.SHA256);
|
||
|
||
fingerprint = FormatFingerprint(fingerprint);
|
||
|
||
DateTimeOffset validFrom =
|
||
new DateTimeOffset(certificate.NotBefore);
|
||
|
||
DateTimeOffset validUntil =
|
||
new DateTimeOffset(certificate.NotAfter);
|
||
|
||
DateTimeOffset now = DateTimeOffset.Now;
|
||
|
||
bool isCurrentlyValid =
|
||
now >= validFrom &&
|
||
now <= validUntil;
|
||
|
||
return new CertificateInfo
|
||
{
|
||
Subject = subject,
|
||
Issuer = issuer,
|
||
ValidFrom = validFrom,
|
||
ValidUntil = validUntil,
|
||
FingerprintSha256 = fingerprint,
|
||
IsCurrentlyValid = isCurrentlyValid
|
||
};
|
||
}
|
||
|
||
private void DisplayCertificateInfo(CertificateInfo certificateInfo)
|
||
{
|
||
lblCertificateSubject.Text = certificateInfo.Subject;
|
||
lblCertificateIssuer.Text = certificateInfo.Issuer;
|
||
lblCertificateExpiry.Text =
|
||
certificateInfo.ValidUntil.ToLocalTime().ToString("dd.MM.yyyy HH:mm");
|
||
lblCertificateFingerprint.Text = certificateInfo.FingerprintSha256;
|
||
|
||
if (certificateInfo.IsCurrentlyValid)
|
||
{
|
||
SetValidityBadge("● GÜLTIG", GreenColor, Color.FromArgb(20, 62, 46));
|
||
return;
|
||
}
|
||
|
||
SetValidityBadge(
|
||
"● ABGELAUFEN / NICHT GÜLTIG",
|
||
RedColor,
|
||
Color.FromArgb(72, 31, 38));
|
||
}
|
||
private static string FormatFingerprint(string fingerprint)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(fingerprint))
|
||
{
|
||
return string.Empty;
|
||
}
|
||
|
||
return string.Join(
|
||
":",
|
||
Enumerable.Range(0, fingerprint.Length / 2)
|
||
.Select(index => fingerprint.Substring(index * 2, 2)));
|
||
}
|
||
private static bool IsSupportedCertificateFile(string filePath)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(filePath))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (!File.Exists(filePath))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
string extension = Path.GetExtension(filePath);
|
||
|
||
string[] supportedExtensions =
|
||
{
|
||
".cer",
|
||
".crt",
|
||
".pem",
|
||
".pfx"
|
||
};
|
||
|
||
return supportedExtensions.Contains(
|
||
extension,
|
||
StringComparer.OrdinalIgnoreCase);
|
||
}
|
||
private void BuildDetailsCard(Panel card)
|
||
{
|
||
Label step = CreateSmallTitle("ERKANNTE INFORMATIONEN");
|
||
step.Location = new Point(22, 18);
|
||
|
||
Label title = CreateCardTitle("Zertifikatsdetails");
|
||
title.Location = new Point(22, 40);
|
||
|
||
lblCertificateValidity = new Label
|
||
{
|
||
Text = "○ NICHT GELADEN",
|
||
ForeColor = MutedTextColor,
|
||
BackColor = Color.FromArgb(39, 52, 73),
|
||
Font = new Font("Segoe UI", 8, FontStyle.Bold),
|
||
AutoSize = true,
|
||
Padding = new Padding(8, 5, 8, 5),
|
||
Anchor = AnchorStyles.Top | AnchorStyles.Right
|
||
};
|
||
|
||
lblCertificateValidity.Location = new Point(300, 40);
|
||
|
||
card.Resize += (_, _) => RepositionValidityBadge();
|
||
|
||
Label subjectLabel = CreateMutedLabel("SUBJECT / CN");
|
||
subjectLabel.Location = new Point(22, 84);
|
||
|
||
lblCertificateSubject = CreateValueLabel("-");
|
||
lblCertificateSubject.Location = new Point(22, 102);
|
||
lblCertificateSubject.AutoEllipsis = true;
|
||
lblCertificateSubject.MaximumSize = new Size(210, 0);
|
||
|
||
Label issuerLabel = CreateMutedLabel("AUSSTELLER");
|
||
issuerLabel.Location = new Point(22, 133);
|
||
|
||
lblCertificateIssuer = CreateValueLabel("-");
|
||
lblCertificateIssuer.Location = new Point(22, 151);
|
||
lblCertificateIssuer.AutoEllipsis = true;
|
||
lblCertificateIssuer.MaximumSize = new Size(210, 0);
|
||
|
||
Label expiryLabel = CreateMutedLabel("GÜLTIG BIS");
|
||
expiryLabel.Location = new Point(22, 182);
|
||
|
||
lblCertificateExpiry = CreateValueLabel("-");
|
||
lblCertificateExpiry.Location = new Point(22, 200);
|
||
|
||
Label fingerprintLabel = CreateMutedLabel("SHA-256 FINGERPRINT");
|
||
fingerprintLabel.Location = new Point(245, 84);
|
||
|
||
lblCertificateFingerprint = CreateValueLabel("-");
|
||
lblCertificateFingerprint.Location = new Point(245, 102);
|
||
lblCertificateFingerprint.AutoSize = false;
|
||
lblCertificateFingerprint.Size = new Size(310, 48);
|
||
|
||
card.Controls.Add(step);
|
||
card.Controls.Add(title);
|
||
card.Controls.Add(lblCertificateValidity);
|
||
|
||
card.Controls.Add(subjectLabel);
|
||
card.Controls.Add(lblCertificateSubject);
|
||
|
||
card.Controls.Add(issuerLabel);
|
||
card.Controls.Add(lblCertificateIssuer);
|
||
|
||
card.Controls.Add(expiryLabel);
|
||
card.Controls.Add(lblCertificateExpiry);
|
||
|
||
card.Controls.Add(fingerprintLabel);
|
||
card.Controls.Add(lblCertificateFingerprint);
|
||
}
|
||
|
||
private Panel BuildTargetsCard()
|
||
{
|
||
Panel card = CreateCard();
|
||
|
||
Label step = CreateSmallTitle("SCHRITT 2");
|
||
step.Location = new Point(22, 18);
|
||
|
||
Label title = CreateCardTitle("Bereitstellungsziele");
|
||
title.Location = new Point(22, 40);
|
||
|
||
Label description = CreateMutedLabel(
|
||
"Wähle aus, auf welche Systeme das Zertifikat verteilt werden soll.");
|
||
description.Location = new Point(22, 72);
|
||
|
||
dgvTargets = new DataGridView
|
||
{
|
||
Location = new Point(22, 148),
|
||
Size = new Size(1000, 205),
|
||
Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right,
|
||
BackgroundColor = CardColor,
|
||
BorderStyle = BorderStyle.None,
|
||
EnableHeadersVisualStyles = false,
|
||
AllowUserToAddRows = false,
|
||
AllowUserToDeleteRows = false,
|
||
AllowUserToResizeRows = false,
|
||
AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill,
|
||
RowHeadersVisible = false,
|
||
SelectionMode = DataGridViewSelectionMode.FullRowSelect,
|
||
MultiSelect = false,
|
||
GridColor = BorderColor
|
||
};
|
||
|
||
dgvTargets.ColumnHeadersDefaultCellStyle = new DataGridViewCellStyle
|
||
{
|
||
BackColor = Color.FromArgb(24, 34, 52),
|
||
ForeColor = MutedTextColor,
|
||
Font = new Font("Segoe UI", 8.5f, FontStyle.Bold),
|
||
Alignment = DataGridViewContentAlignment.MiddleLeft
|
||
};
|
||
|
||
dgvTargets.DefaultCellStyle = new DataGridViewCellStyle
|
||
{
|
||
BackColor = CardColor,
|
||
ForeColor = TextColor,
|
||
SelectionBackColor = Color.FromArgb(40, 57, 82),
|
||
SelectionForeColor = TextColor,
|
||
Font = new Font("Segoe UI", 9),
|
||
Padding = new Padding(4, 0, 4, 0)
|
||
};
|
||
|
||
dgvTargets.AlternatingRowsDefaultCellStyle = new DataGridViewCellStyle
|
||
{
|
||
BackColor = Color.FromArgb(28, 39, 57),
|
||
ForeColor = TextColor
|
||
};
|
||
|
||
dgvTargets.ColumnHeadersHeight = 34;
|
||
dgvTargets.RowTemplate.Height = 32;
|
||
|
||
dgvTargets.Columns.Add(new DataGridViewCheckBoxColumn
|
||
{
|
||
Name = "Selected",
|
||
HeaderText = "",
|
||
FillWeight = 25
|
||
});
|
||
|
||
dgvTargets.Columns.Add("Target", "Ziel");
|
||
dgvTargets.Columns.Add("Environment", "Umgebung");
|
||
dgvTargets.Columns.Add("Container", "Container / Service");
|
||
dgvTargets.Columns.Add("Tls", "TLS-Prüfung");
|
||
dgvTargets.Columns.Add("Status", "Status");
|
||
|
||
dgvTargets.CurrentCellDirtyStateChanged += (_, _) =>
|
||
{
|
||
if (dgvTargets.IsCurrentCellDirty
|
||
&& dgvTargets.CurrentCell is DataGridViewCheckBoxCell)
|
||
{
|
||
dgvTargets.CommitEdit(DataGridViewDataErrorContexts.Commit);
|
||
}
|
||
};
|
||
|
||
dgvTargets.CellValueChanged += (_, e) =>
|
||
{
|
||
if (e.ColumnIndex >= 0
|
||
&& dgvTargets.Columns[e.ColumnIndex].Name == "Selected")
|
||
{
|
||
RefreshActionButtonStates();
|
||
}
|
||
};
|
||
|
||
card.Resize += (_, _) =>
|
||
{
|
||
dgvTargets.Width = card.Width - 44;
|
||
};
|
||
|
||
card.Controls.Add(step);
|
||
card.Controls.Add(title);
|
||
card.Controls.Add(description);
|
||
card.Controls.Add(dgvTargets);
|
||
|
||
// ---- Sonic Management Console Discovery ----
|
||
BuildSonicDiscoveryRow(card);
|
||
|
||
return card;
|
||
}
|
||
|
||
private Panel BuildActionPanel()
|
||
{
|
||
Panel panel = new Panel
|
||
{
|
||
BackColor = BackgroundColor
|
||
};
|
||
|
||
btnCancelRun = CreateSecondaryButton("Abbrechen");
|
||
btnCancelRun.Size = new Size(120, 42);
|
||
btnCancelRun.Anchor = AnchorStyles.Top | AnchorStyles.Right;
|
||
btnCancelRun.Enabled = false;
|
||
btnCancelRun.Click += (_, _) =>
|
||
{
|
||
_runCts?.Cancel();
|
||
SetStatus("Abbruch angefordert…", isError: false);
|
||
};
|
||
|
||
btnValidate = CreateSecondaryButton("Prüfung durchführen");
|
||
btnValidate.Size = new Size(175, 42);
|
||
btnValidate.Anchor = AnchorStyles.Top | AnchorStyles.Right;
|
||
btnValidate.Click += async (_, _) => await RunValidationAsync();
|
||
|
||
btnRestartOnly = CreateSecondaryButton("ESB neu starten");
|
||
btnRestartOnly.Size = new Size(150, 42);
|
||
btnRestartOnly.Anchor = AnchorStyles.Top | AnchorStyles.Right;
|
||
btnRestartOnly.Click += async (_, _) => await RunRestartOnlyAsync();
|
||
|
||
btnDeploy = CreatePrimaryButton("Deployment starten");
|
||
btnDeploy.Size = new Size(170, 42);
|
||
btnDeploy.Anchor = AnchorStyles.Top | AnchorStyles.Right;
|
||
btnDeploy.Click += async (_, _) => await RunDeploymentAsync();
|
||
|
||
panel.Controls.Add(btnCancelRun);
|
||
panel.Controls.Add(btnValidate);
|
||
panel.Controls.Add(btnRestartOnly);
|
||
panel.Controls.Add(btnDeploy);
|
||
|
||
panel.Resize += (_, _) =>
|
||
{
|
||
btnDeploy.Left = panel.Width - btnDeploy.Width;
|
||
btnRestartOnly.Left = btnDeploy.Left - btnRestartOnly.Width - 12;
|
||
btnValidate.Left = btnRestartOnly.Left - btnValidate.Width - 12;
|
||
btnCancelRun.Left = btnValidate.Left - btnCancelRun.Width - 12;
|
||
};
|
||
|
||
RefreshActionButtonStates();
|
||
return panel;
|
||
}
|
||
|
||
private Panel BuildStatusBar()
|
||
{
|
||
Panel panel = new Panel
|
||
{
|
||
Height = 28,
|
||
BackColor = BackgroundColor
|
||
};
|
||
|
||
lblStatusDot = new Label
|
||
{
|
||
Text = "●",
|
||
ForeColor = GreenColor,
|
||
Font = new Font("Segoe UI", 10),
|
||
AutoSize = true,
|
||
Location = new Point(0, 4)
|
||
};
|
||
|
||
lblStatus = new Label
|
||
{
|
||
Text = "Warte auf Zertifikatsauswahl",
|
||
ForeColor = MutedTextColor,
|
||
Font = new Font("Segoe UI", 9),
|
||
AutoSize = true,
|
||
Location = new Point(18, 5)
|
||
};
|
||
|
||
panel.Controls.Add(lblStatusDot);
|
||
panel.Controls.Add(lblStatus);
|
||
|
||
return panel;
|
||
}
|
||
|
||
private async Task LoadTargetsAsync()
|
||
{
|
||
try
|
||
{
|
||
SetStatus("Lade Bereitstellungsziele…", isError: false);
|
||
(ITargetRepository repository, string loadMessage) =
|
||
await TargetRepositoryFactory.CreateAsync(_settings);
|
||
|
||
_loadedTargets = await repository.GetActiveTargetsAsync();
|
||
BindTargetsToGrid(_loadedTargets);
|
||
SetStatus(loadMessage, isError: false);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_loadedTargets = [];
|
||
dgvTargets.Rows.Clear();
|
||
SetStatus($"Ziele konnten nicht geladen werden: {ex.Message}", isError: true);
|
||
}
|
||
finally
|
||
{
|
||
RefreshActionButtonStates();
|
||
}
|
||
}
|
||
|
||
private void BindTargetsToGrid(IReadOnlyList<DeploymentTarget> targets)
|
||
{
|
||
dgvTargets.Rows.Clear();
|
||
|
||
foreach (DeploymentTarget target in targets)
|
||
{
|
||
int tlsPort = target.TlsPort is > 0 ? target.TlsPort.Value : 443;
|
||
string tls = string.IsNullOrWhiteSpace(target.TlsHost)
|
||
? "—"
|
||
: $"{target.TlsHost}:{tlsPort}";
|
||
|
||
int rowIndex = dgvTargets.Rows.Add(
|
||
false,
|
||
target.Name,
|
||
target.Environment,
|
||
target.ContainerName,
|
||
tls,
|
||
"Bereit");
|
||
|
||
dgvTargets.Rows[rowIndex].Tag = target;
|
||
}
|
||
}
|
||
|
||
private List<DeploymentTarget> GetSelectedTargets()
|
||
{
|
||
List<DeploymentTarget> selected = [];
|
||
|
||
foreach (DataGridViewRow row in dgvTargets.Rows)
|
||
{
|
||
if (row.Cells["Selected"].Value is true && row.Tag is DeploymentTarget target)
|
||
{
|
||
selected.Add(target);
|
||
}
|
||
}
|
||
|
||
return selected;
|
||
}
|
||
|
||
private void SetTargetRowStatus(int targetId, string status)
|
||
{
|
||
foreach (DataGridViewRow row in dgvTargets.Rows)
|
||
{
|
||
if (row.Tag is DeploymentTarget target && target.Id == targetId)
|
||
{
|
||
row.Cells["Status"].Value = status;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
private void RefreshActionButtonStates()
|
||
{
|
||
if (btnValidate is null || btnDeploy is null || btnCancelRun is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
bool idle = !_isOperationRunning;
|
||
bool hasCertificate = isCertificateFileLoaded && _loadedCertificateInfo is not null;
|
||
bool hasTargets = _loadedTargets.Count > 0;
|
||
bool hasSelection = dgvTargets is not null && GetSelectedTargets().Count > 0;
|
||
|
||
if (btnSelectFile is not null)
|
||
{
|
||
btnSelectFile.Enabled = idle;
|
||
}
|
||
|
||
btnCancelRun.Enabled = _isOperationRunning;
|
||
btnValidate.Enabled = idle && hasCertificate && hasTargets;
|
||
btnDeploy.Enabled = idle && hasCertificate && hasSelection;
|
||
if (btnRestartOnly is not null)
|
||
{
|
||
btnRestartOnly.Enabled = idle && hasSelection;
|
||
}
|
||
|
||
if (dgvTargets is not null)
|
||
{
|
||
dgvTargets.Enabled = idle;
|
||
}
|
||
|
||
if (btnLoadFromSonic is not null)
|
||
{
|
||
btnLoadFromSonic.Enabled = idle && _sonicDiscovery.HasConnections;
|
||
}
|
||
}
|
||
|
||
private void SetStatus(string message, bool isError)
|
||
{
|
||
if (lblStatus is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
lblStatus.Text = message;
|
||
lblStatus.ForeColor = isError ? RedColor : MutedTextColor;
|
||
if (lblStatusDot is not null)
|
||
{
|
||
lblStatusDot.ForeColor = isError ? RedColor : (_isOperationRunning ? GoldColor : GreenColor);
|
||
}
|
||
}
|
||
|
||
private void SetOperationRunning(bool running)
|
||
{
|
||
_isOperationRunning = running;
|
||
RefreshActionButtonStates();
|
||
}
|
||
|
||
private void ShowPreflightIssues(PreflightValidationResult result, string statusMessage, string caption)
|
||
{
|
||
string details = string.Join(Environment.NewLine, result.Issues.Select(i => "• " + i.Message));
|
||
SetStatus(statusMessage, isError: true);
|
||
MessageBox.Show(this, details, caption, MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||
}
|
||
|
||
private Task RunValidationAsync()
|
||
{
|
||
if (_isOperationRunning)
|
||
{
|
||
return Task.CompletedTask;
|
||
}
|
||
|
||
List<DeploymentTarget> selected = GetSelectedTargets();
|
||
PreflightValidationResult result = _orchestrator.ValidatePreflight(
|
||
txtCertificatePath.Text,
|
||
_loadedCertificateInfo,
|
||
selected);
|
||
|
||
HashSet<int> failedTargetIds = result.Issues
|
||
.Where(i => i.TargetId is not null)
|
||
.Select(i => i.TargetId!.Value)
|
||
.ToHashSet();
|
||
|
||
foreach (DataGridViewRow row in dgvTargets.Rows)
|
||
{
|
||
if (row.Tag is not DeploymentTarget target)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (failedTargetIds.Contains(target.Id))
|
||
{
|
||
SetTargetRowStatus(target.Id, "Prüfung fehlgeschlagen");
|
||
}
|
||
else if (row.Cells["Selected"].Value is true)
|
||
{
|
||
SetTargetRowStatus(target.Id, "Bereit");
|
||
}
|
||
}
|
||
|
||
if (!result.IsValid)
|
||
{
|
||
ShowPreflightIssues(result, "Vorabprüfung fehlgeschlagen.", "Vorabprüfung");
|
||
return Task.CompletedTask;
|
||
}
|
||
|
||
UpdateToNextStep(3);
|
||
SetStatus($"Vorabprüfung ok – {selected.Count} Ziel(e) bereit.", isError: false);
|
||
MessageBox.Show(
|
||
this,
|
||
$"Alle Voraussetzungen sind erfüllt.\nAusgewählte Ziele: {selected.Count}",
|
||
"Vorabprüfung",
|
||
MessageBoxButtons.OK,
|
||
MessageBoxIcon.Information);
|
||
return Task.CompletedTask;
|
||
}
|
||
|
||
private async Task RunRestartOnlyAsync()
|
||
{
|
||
if (_isOperationRunning)
|
||
{
|
||
return;
|
||
}
|
||
|
||
List<DeploymentTarget> selected = GetSelectedTargets();
|
||
PreflightValidationResult preflight = _orchestrator.ValidateRestartOnly(selected);
|
||
|
||
if (!preflight.IsValid)
|
||
{
|
||
ShowPreflightIssues(
|
||
preflight,
|
||
"Neustart blockiert – Vorabprüfung fehlgeschlagen.",
|
||
"ESB neu starten");
|
||
return;
|
||
}
|
||
|
||
string targetNames = string.Join(", ", selected.Select(t =>
|
||
string.IsNullOrWhiteSpace(t.ContainerName) ? t.Name : t.ContainerName));
|
||
|
||
DialogResult confirm = MessageBox.Show(
|
||
this,
|
||
$"Container wirklich neu starten?\n\n" +
|
||
$"Ziele: {targetNames}\n" +
|
||
"Domain bleibt aus appsettings (z.B. proalpha-test).\n" +
|
||
"Erfolg nur bei verifiziertem Prozess-Neustart (sichtbar in SMC).",
|
||
"ESB neu starten",
|
||
MessageBoxButtons.YesNo,
|
||
MessageBoxIcon.Warning);
|
||
|
||
if (confirm != DialogResult.Yes)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_runCts?.Dispose();
|
||
_runCts = new CancellationTokenSource();
|
||
SetOperationRunning(true);
|
||
UpdateToNextStep(3);
|
||
SetStatus($"ESB-Neustart läuft für {selected.Count} Ziel(e)…", isError: false);
|
||
|
||
Progress<TargetProgressUpdate> progress = new(update =>
|
||
{
|
||
SetTargetRowStatus(update.TargetId, update.StatusText);
|
||
SetStatus(update.StatusText, isError: update.SuccessHint == false);
|
||
});
|
||
|
||
try
|
||
{
|
||
DeploymentRunResult runResult = await _orchestrator.RestartOnlyAsync(
|
||
selected,
|
||
progress,
|
||
_runCts.Token);
|
||
|
||
foreach (TargetStepResult targetResult in runResult.TargetResults)
|
||
{
|
||
SetTargetRowStatus(targetResult.TargetId, targetResult.StatusText);
|
||
}
|
||
|
||
string details = string.Join(
|
||
Environment.NewLine + Environment.NewLine,
|
||
runResult.TargetResults.Select(r =>
|
||
$"{r.TargetName}: {r.StatusText}" +
|
||
(string.IsNullOrWhiteSpace(r.Detail) ? string.Empty : Environment.NewLine + r.Detail)));
|
||
|
||
SetStatus(
|
||
runResult.OverallSuccess
|
||
? $"Neustart verifiziert ({runResult.TargetResults.Count} Ziel(e))."
|
||
: "Neustart fehlgeschlagen oder nicht verifiziert.",
|
||
isError: !runResult.OverallSuccess);
|
||
|
||
MessageBox.Show(
|
||
this,
|
||
details,
|
||
runResult.OverallSuccess ? "Neustart verifiziert" : "Neustart fehlgeschlagen",
|
||
MessageBoxButtons.OK,
|
||
runResult.OverallSuccess ? MessageBoxIcon.Information : MessageBoxIcon.Warning);
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
SetStatus("Neustart abgebrochen.", isError: true);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
SetStatus($"Neustart fehlgeschlagen: {ex.Message}", isError: true);
|
||
MessageBox.Show(this, ex.Message, "ESB neu starten", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
}
|
||
finally
|
||
{
|
||
SetOperationRunning(false);
|
||
_runCts?.Dispose();
|
||
_runCts = null;
|
||
}
|
||
}
|
||
|
||
private async Task RunDeploymentAsync()
|
||
{
|
||
if (_isOperationRunning || _loadedCertificateInfo is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
List<DeploymentTarget> selected = GetSelectedTargets();
|
||
PreflightValidationResult preflight = _orchestrator.ValidatePreflight(
|
||
txtCertificatePath.Text,
|
||
_loadedCertificateInfo,
|
||
selected);
|
||
|
||
if (!preflight.IsValid)
|
||
{
|
||
ShowPreflightIssues(
|
||
preflight,
|
||
"Deployment blockiert – Vorabprüfung fehlgeschlagen.",
|
||
"Deployment");
|
||
return;
|
||
}
|
||
|
||
_runCts?.Dispose();
|
||
_runCts = new CancellationTokenSource();
|
||
SetOperationRunning(true);
|
||
UpdateToNextStep(3);
|
||
SetStatus($"Deployment läuft für {selected.Count} Ziel(e)…", isError: false);
|
||
|
||
Progress<TargetProgressUpdate> progress = new(update =>
|
||
{
|
||
SetTargetRowStatus(update.TargetId, update.StatusText);
|
||
SetStatus(update.StatusText, isError: update.SuccessHint == false);
|
||
});
|
||
|
||
try
|
||
{
|
||
DeploymentRunResult runResult = await _orchestrator.RunAsync(
|
||
txtCertificatePath.Text,
|
||
_loadedCertificateInfo,
|
||
selected,
|
||
progress,
|
||
_runCts.Token);
|
||
|
||
foreach (TargetStepResult targetResult in runResult.TargetResults)
|
||
{
|
||
SetTargetRowStatus(targetResult.TargetId, targetResult.StatusText);
|
||
}
|
||
|
||
UpdateToNextStep(4);
|
||
SetStatus(
|
||
runResult.OverallSuccess
|
||
? $"Deployment erfolgreich ({runResult.TargetResults.Count} Ziel(e))."
|
||
: "Deployment mit Fehlern beendet. Details in der Status-Spalte / Log.",
|
||
isError: !runResult.OverallSuccess);
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
SetStatus("Deployment abgebrochen.", isError: true);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
SetStatus($"Deployment fehlgeschlagen: {ex.Message}", isError: true);
|
||
MessageBox.Show(
|
||
this,
|
||
ex.Message,
|
||
"Deployment",
|
||
MessageBoxButtons.OK,
|
||
MessageBoxIcon.Error);
|
||
}
|
||
finally
|
||
{
|
||
SetOperationRunning(false);
|
||
_runCts?.Dispose();
|
||
_runCts = null;
|
||
}
|
||
}
|
||
|
||
private Panel CreateCard()
|
||
{
|
||
return new Panel
|
||
{
|
||
BackColor = CardColor,
|
||
BorderStyle = BorderStyle.FixedSingle,
|
||
Padding = new Padding(20)
|
||
};
|
||
}
|
||
|
||
private Label CreateSmallTitle(string text)
|
||
{
|
||
return new Label
|
||
{
|
||
Text = text,
|
||
ForeColor = BlueColor,
|
||
Font = new Font("Segoe UI", 8, FontStyle.Bold),
|
||
AutoSize = true
|
||
};
|
||
}
|
||
|
||
private Label CreateCardTitle(string text)
|
||
{
|
||
return new Label
|
||
{
|
||
Text = text,
|
||
ForeColor = TextColor,
|
||
Font = new Font("Segoe UI", 14, FontStyle.Bold),
|
||
AutoSize = true
|
||
};
|
||
}
|
||
|
||
private Label CreateMutedLabel(string text)
|
||
{
|
||
return new Label
|
||
{
|
||
Text = text,
|
||
ForeColor = MutedTextColor,
|
||
Font = new Font("Segoe UI", 9),
|
||
AutoSize = true
|
||
};
|
||
}
|
||
|
||
private Label CreateValueLabel(string text)
|
||
{
|
||
return new Label
|
||
{
|
||
Text = text,
|
||
ForeColor = TextColor,
|
||
Font = new Font("Segoe UI", 9.5f, FontStyle.Regular),
|
||
AutoSize = true
|
||
};
|
||
}
|
||
|
||
private Button CreatePrimaryButton(string text)
|
||
{
|
||
Button button = new Button
|
||
{
|
||
Text = text,
|
||
BackColor = GoldColor,
|
||
ForeColor = Color.FromArgb(30, 25, 10),
|
||
FlatStyle = FlatStyle.Flat,
|
||
FlatAppearance =
|
||
{
|
||
BorderSize = 0,
|
||
MouseOverBackColor = Color.FromArgb(250, 204, 21)
|
||
},
|
||
Font = new Font("Segoe UI", 9.5f, FontStyle.Bold),
|
||
Cursor = Cursors.Hand
|
||
};
|
||
|
||
return button;
|
||
}
|
||
|
||
private Button CreateSecondaryButton(string text)
|
||
{
|
||
Button button = new Button
|
||
{
|
||
Text = text,
|
||
BackColor = Color.FromArgb(39, 52, 73),
|
||
ForeColor = TextColor,
|
||
FlatStyle = FlatStyle.Flat,
|
||
FlatAppearance =
|
||
{
|
||
BorderColor = BorderColor,
|
||
BorderSize = 1,
|
||
MouseOverBackColor = Color.FromArgb(54, 69, 94)
|
||
},
|
||
Font = new Font("Segoe UI", 9, FontStyle.Bold),
|
||
Cursor = Cursors.Hand
|
||
};
|
||
|
||
return button;
|
||
}
|
||
|
||
// ---------------------------------------------------------------
|
||
// Sonic Management Console – Container-Discovery
|
||
// ---------------------------------------------------------------
|
||
|
||
private void BuildSonicDiscoveryRow(Panel card)
|
||
{
|
||
Label sonicLabel = new Label
|
||
{
|
||
Text = "Sonic Management:",
|
||
ForeColor = MutedTextColor,
|
||
Font = new Font("Segoe UI", 8.5f, FontStyle.Bold),
|
||
AutoSize = true,
|
||
Location = new Point(22, 108)
|
||
};
|
||
|
||
cmbSonicConnection = new ComboBox
|
||
{
|
||
Location = new Point(160, 104),
|
||
Size = new Size(220, 26),
|
||
DropDownStyle = ComboBoxStyle.DropDownList,
|
||
BackColor = Color.FromArgb(20, 30, 48),
|
||
ForeColor = TextColor,
|
||
FlatStyle = FlatStyle.Flat,
|
||
Font = new Font("Segoe UI", 9)
|
||
};
|
||
|
||
foreach (SonicConnection conn in _sonicDiscovery.Connections)
|
||
{
|
||
cmbSonicConnection.Items.Add(conn.Name);
|
||
}
|
||
|
||
btnLoadFromSonic = CreateSecondaryButton("Container laden");
|
||
btnLoadFromSonic.Location = new Point(392, 103);
|
||
btnLoadFromSonic.Size = new Size(148, 28);
|
||
btnLoadFromSonic.Enabled = _sonicDiscovery.HasConnections;
|
||
btnLoadFromSonic.Click += async (_, _) => await LoadFromSonicAsync();
|
||
|
||
lblSonicStatus = new Label
|
||
{
|
||
ForeColor = MutedTextColor,
|
||
Font = new Font("Segoe UI", 8.5f),
|
||
AutoSize = true,
|
||
MaximumSize = new Size(720, 0),
|
||
Location = new Point(552, 100)
|
||
};
|
||
|
||
cmbSonicConnection.SelectedIndexChanged += (_, _) => RefreshSonicStatusLine();
|
||
|
||
if (cmbSonicConnection.Items.Count > 0)
|
||
{
|
||
cmbSonicConnection.SelectedIndex = 0;
|
||
}
|
||
else
|
||
{
|
||
RefreshSonicStatusLine();
|
||
}
|
||
|
||
card.Controls.Add(sonicLabel);
|
||
card.Controls.Add(cmbSonicConnection);
|
||
card.Controls.Add(btnLoadFromSonic);
|
||
card.Controls.Add(lblSonicStatus);
|
||
}
|
||
|
||
private void RefreshSonicStatusLine(string? suffix = null)
|
||
{
|
||
if (!_sonicDiscovery.HasConnections)
|
||
{
|
||
lblSonicStatus.Text = "Keine Sonic-Verbindung in appsettings konfiguriert";
|
||
lblSonicStatus.ForeColor = RedColor;
|
||
return;
|
||
}
|
||
|
||
string? selectedName = cmbSonicConnection?.SelectedItem?.ToString();
|
||
SonicConnection conn = _sonicDiscovery.Connections
|
||
.FirstOrDefault(c => string.Equals(c.Name, selectedName, StringComparison.OrdinalIgnoreCase))
|
||
?? _sonicDiscovery.Connections[0];
|
||
|
||
(string sonicHome, string? javaExe) = new SonicMfApiExecutor(conn).ResolveRuntimePaths();
|
||
string javaDisplay = string.IsNullOrWhiteSpace(javaExe) ? "java=?" : javaExe;
|
||
string baseText =
|
||
$"{conn.ManagementModeDisplay} | SonicHome={sonicHome} | {javaDisplay}";
|
||
if (!string.IsNullOrWhiteSpace(suffix))
|
||
{
|
||
baseText = $"{suffix} | {baseText}";
|
||
}
|
||
|
||
lblSonicStatus.Text = baseText;
|
||
lblSonicStatus.ForeColor = string.IsNullOrWhiteSpace(javaExe) ? GoldColor : GreenColor;
|
||
}
|
||
|
||
private static string TruncateStatus(string? text, int max = 120)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(text))
|
||
{
|
||
return "unbekannt";
|
||
}
|
||
|
||
string oneLine = text.Replace('\r', ' ').Replace('\n', ' ').Trim();
|
||
return oneLine.Length <= max ? oneLine : oneLine[..max] + "…";
|
||
}
|
||
|
||
private async Task LoadFromSonicAsync()
|
||
{
|
||
if (_isOperationRunning) return;
|
||
|
||
string? selectedConnection = cmbSonicConnection?.SelectedItem?.ToString();
|
||
if (string.IsNullOrWhiteSpace(selectedConnection)) return;
|
||
|
||
btnLoadFromSonic.Enabled = false;
|
||
lblSonicStatus.Text = $"Verbinde mit {selectedConnection}…";
|
||
lblSonicStatus.ForeColor = GoldColor;
|
||
|
||
try
|
||
{
|
||
using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30));
|
||
SonicDiscoveryResult result = await _sonicDiscovery.DiscoverAsync(
|
||
selectedConnection,
|
||
_loadedTargets,
|
||
cts.Token);
|
||
|
||
if (!result.Success)
|
||
{
|
||
RefreshSonicStatusLine($"Fehler: {TruncateStatus(result.ErrorMessage)}");
|
||
lblSonicStatus.ForeColor = RedColor;
|
||
SetStatus($"Sonic-Discovery fehlgeschlagen: {result.ErrorMessage}", isError: true);
|
||
return;
|
||
}
|
||
|
||
// Grid mit entdeckten + konfigurierten Containern befüllen
|
||
_loadedTargets = result.DiscoveredTargets;
|
||
BindTargetsToGrid(result.DiscoveredTargets);
|
||
|
||
int total = result.DiscoveredTargets.Count;
|
||
int remote = result.RawContainerNames.Count;
|
||
string domain = string.IsNullOrWhiteSpace(result.DomainName)
|
||
? selectedConnection
|
||
: result.DomainName;
|
||
|
||
if (total == 0)
|
||
{
|
||
RefreshSonicStatusLine($"Verb. ok – 0 Container '{domain}'");
|
||
lblSonicStatus.ForeColor = GoldColor;
|
||
SetStatus(
|
||
result.ErrorMessage
|
||
?? $"Sonic Domain '{domain}': keine Container. KnownContainers/SonicHome in appsettings prüfen.",
|
||
isError: true);
|
||
}
|
||
else if (remote == 0)
|
||
{
|
||
RefreshSonicStatusLine($"Verb. ok – 0 remote, {total} Config '{domain}'");
|
||
lblSonicStatus.ForeColor = GoldColor;
|
||
SetStatus(
|
||
$"Domain '{domain}': Remote leer – {total} Ziel(e) aus appsettings/KnownContainers. "
|
||
+ (result.ErrorMessage ?? string.Empty),
|
||
isError: false);
|
||
}
|
||
else
|
||
{
|
||
RefreshSonicStatusLine($"Domain '{domain}': {remote} remote, {total} gesamt");
|
||
SetStatus($"Sonic Domain '{domain}': {total} Ziel(e) geladen ({remote} remote).", isError: false);
|
||
}
|
||
|
||
MarkUnconfiguredRows();
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
RefreshSonicStatusLine("Timeout");
|
||
lblSonicStatus.ForeColor = RedColor;
|
||
SetStatus($"Sonic-Discovery Timeout ({selectedConnection})", isError: true);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
RefreshSonicStatusLine($"Fehler: {TruncateStatus(ex.Message)}");
|
||
lblSonicStatus.ForeColor = RedColor;
|
||
SetStatus($"Sonic-Discovery Fehler: {ex.Message}", isError: true);
|
||
}
|
||
finally
|
||
{
|
||
btnLoadFromSonic.Enabled = _sonicDiscovery.HasConnections && !_isOperationRunning;
|
||
RefreshActionButtonStates();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Färbt Zeilen mit unvollständiger Konfiguration (kein TargetDirectory) orange ein
|
||
/// damit erkennbar ist, dass diese Container noch konfiguriert werden müssen.
|
||
/// </summary>
|
||
private void MarkUnconfiguredRows()
|
||
{
|
||
foreach (DataGridViewRow row in dgvTargets.Rows)
|
||
{
|
||
if (row.Tag is not DeploymentTarget target) continue;
|
||
|
||
if (string.IsNullOrWhiteSpace(target.TargetDirectory))
|
||
{
|
||
row.DefaultCellStyle.ForeColor = GoldColor;
|
||
if (row.Cells["Status"] is DataGridViewCell statusCell)
|
||
{
|
||
statusCell.Value = "⚠ Pfad fehlt";
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|