Phase 2: Plattformneutralen Hosting-Kern extrahiert
Neues Projekt Predictalytics.Hosting nimmt auf, was bisher im windows-gebundenen WinFormsHost feststeckte, aber portabel ist: - PredictalyticsHost (aus EmbeddedWebServer): Kestrel- und Worker-Lifecycle, Wartungsaktionen, DB-Groesse. Meldet Zustandswechsel ueber StateChanged. - PredictalyticsOptions (aus AppSettings): ohne WinForms-Bezug. Die System.ComponentModel-Attribute sind plattformneutral und bleiben, damit das PropertyGrid Gruppen und Beschreibungen behaelt. - LoggingSetup (aus Program.cs): Serilog-Aufbau, Terminal-Sink als optionale Action statt fester RichTextBox. - LicenseGuard: GUI-frei. Periodische Revalidierung ueber PeriodicTimer statt WinForms-Timer, Abbruch ueber Callback statt Application.Exit. Der interaktive Dialogaufruf bleibt als LicenseGate im WinForms-Host. - WatchdogHeartbeatService unveraendert verschoben. Infrastructure: RichTextBoxSink -> DelegateSink umbenannt (war nie WinForms-abhaengig, nur missverstaendlich benannt). Einstellungen liegen jetzt unter %APPDATA%/Predictalytics bzw. ~/.config/Predictalytics statt neben der Programmdatei, mit einmaliger Uebernahme aus dem alten Ort. Das Installationsverzeichnis ist unter Linux ueblicherweise nicht beschreibbar. wwwroot wird ueber einen Content-Eintrag neben die Programmdatei kopiert; die frueheren Pfad-Heuristiken entfallen. Hosting und WinFormsHost nutzen Microsoft.NET.Sdk statt Sdk.Web: der Web-SDK globbt wwwroot automatisch als Static Web Asset und kollidiert mit dem Content-Eintrag. WebApplication kommt ueber FrameworkReference. Neu konfigurierbar (verhaltensgleiche Defaults): WebserverHost fuer die Kestrel-Bind-Adresse, DbSslMode fuer die MySQL-Verschluesselung. explorer.exe-Aufrufe durch ProcessStartInfo mit UseShellExecute ersetzt — funktioniert unter Windows und Linux. Build: 0 Fehler. Tests: 100 bestanden, 0 Fehler, 1 uebersprungen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Predictalytics.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Laufzeit-Konfiguration der Anwendung.
|
||||
/// <para>
|
||||
/// Die <see cref="System.ComponentModel"/>-Attribute sind plattformneutral (Teil der
|
||||
/// Basisbibliothek) und werden vom WinForms-PropertyGrid ausgewertet. Die Avalonia-Shell
|
||||
/// kann sie fuer generierte Beschriftungen nutzen oder ignorieren.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class PredictalyticsOptions
|
||||
{
|
||||
private const string FileName = "settings.json";
|
||||
|
||||
// ─── Webserver ───
|
||||
|
||||
[Category("Webserver")]
|
||||
[DisplayName("Port")]
|
||||
[Description("Der Port, über den die WebUI und API erreichbar sind.")]
|
||||
[DefaultValue(5000)]
|
||||
public int WebserverPort { get; set; } = 5000;
|
||||
|
||||
[Category("Webserver")]
|
||||
[DisplayName("Bind-Adresse")]
|
||||
[Description("Netzwerkschnittstelle, auf der Kestrel lauscht. 'localhost' = nur lokal (Standard), " +
|
||||
"'0.0.0.0' = von außen erreichbar. Bei 0.0.0.0 muss die API abgesichert werden!")]
|
||||
[DefaultValue("localhost")]
|
||||
public string WebserverHost { get; set; } = "localhost";
|
||||
|
||||
[Category("Webserver")]
|
||||
[DisplayName("Database Debug")]
|
||||
[Description("Wenn aktiv, werden detaillierte Verbindungsinformationen im Terminal angezeigt.")]
|
||||
[DefaultValue(false)]
|
||||
public bool DbConnectionDebug { get; set; } = false;
|
||||
|
||||
// ─── Egress ───
|
||||
|
||||
private string _egressChannelsText = "";
|
||||
|
||||
[Category("Egress (Proxy/IP)")]
|
||||
[DisplayName("Egress Channels")]
|
||||
[Description("Liste der Egress-Kanäle im Format: id|type|value (Zeilengetrennt). " +
|
||||
"Beispiel: prox-1|Proxy|http://user:pass@proxy:8080\nip-1|SourceIp|192.168.1.100")]
|
||||
public string EgressChannelsText { get => _egressChannelsText; set => _egressChannelsText = value ?? ""; }
|
||||
|
||||
// ─── Watchdog ───
|
||||
|
||||
[Category("Watchdog")]
|
||||
[DisplayName("Enabled")]
|
||||
[Description("Sendet periodische Heartbeats an den externen Watchdog-Server (Dead-Man's-Switch). Benötigt einen API Key.")]
|
||||
[DefaultValue(true)]
|
||||
public bool WatchdogEnabled { get; set; } = true;
|
||||
|
||||
[Category("Watchdog")]
|
||||
[DisplayName("Server URL")]
|
||||
[Description("Basis-URL des Watchdog-Servers.")]
|
||||
[DefaultValue("https://watchdog.mhdf.de")]
|
||||
public string WatchdogUrl { get; set; } = "https://watchdog.mhdf.de";
|
||||
|
||||
[Category("Watchdog")]
|
||||
[DisplayName("API Key")]
|
||||
[Description("Shared Key oder Agent-Token des Watchdog-Servers (X-Watchdog-Key). Ohne Key werden keine Heartbeats gesendet.")]
|
||||
[PasswordPropertyText(true)]
|
||||
public string WatchdogApiKey { get; set; } = "";
|
||||
|
||||
[Category("Watchdog")]
|
||||
[DisplayName("Source")]
|
||||
[Description("Eindeutiger Monitor-Name dieses Dienstes im Watchdog-Dashboard.")]
|
||||
[DefaultValue("Predictalytics")]
|
||||
public string WatchdogSource { get; set; } = "Predictalytics";
|
||||
|
||||
[Category("Watchdog")]
|
||||
[DisplayName("Instance")]
|
||||
[Description("Instanz-Kennung, falls mehrere Predictalytics-Instanzen laufen.")]
|
||||
[DefaultValue("default")]
|
||||
public string WatchdogInstance { get; set; } = "default";
|
||||
|
||||
[Category("Watchdog")]
|
||||
[DisplayName("Interval (Sekunden)")]
|
||||
[Description("Sende-Takt der Heartbeats. Der Watchdog alarmiert, wenn ~1,5× dieses Intervall + 30 s ohne Heartbeat vergehen.")]
|
||||
[DefaultValue(60)]
|
||||
public int WatchdogIntervalSeconds { get; set; } = 60;
|
||||
|
||||
// ─── Database ───
|
||||
|
||||
private string _dbServer = "localhost";
|
||||
private string _dbName = "";
|
||||
private string _dbUser = "";
|
||||
private string _dbPassword = "";
|
||||
|
||||
[Category("Database")]
|
||||
[DisplayName("Server")]
|
||||
public string DbServer { get => _dbServer; set => _dbServer = string.IsNullOrWhiteSpace(value) ? _dbServer : value.Trim(); }
|
||||
|
||||
[Category("Database")]
|
||||
[DisplayName("Database")]
|
||||
public string DbName { get => _dbName; set => _dbName = string.IsNullOrWhiteSpace(value) ? _dbName : value.Trim(); }
|
||||
|
||||
[Category("Database")]
|
||||
[DisplayName("User")]
|
||||
public string DbUser { get => _dbUser; set => _dbUser = string.IsNullOrWhiteSpace(value) ? _dbUser : value.Trim(); }
|
||||
|
||||
[Category("Database")]
|
||||
[DisplayName("Password")]
|
||||
[PasswordPropertyText(true)]
|
||||
public string DbPassword { get => _dbPassword; set => _dbPassword = string.IsNullOrWhiteSpace(value) ? _dbPassword : value.Trim(); }
|
||||
|
||||
[Category("Database")]
|
||||
[DisplayName("SSL Mode")]
|
||||
[Description("Verschlüsselung der MySQL-Verbindung. 'None' = unverschlüsselt (bisheriges Verhalten), " +
|
||||
"'Preferred' = verschlüsselt wenn der Server es unterstützt. Bei entfernten Servern 'Preferred' empfohlen.")]
|
||||
[DefaultValue(MySqlConnector.MySqlSslMode.None)]
|
||||
public MySqlConnector.MySqlSslMode DbSslMode { get; set; } = MySqlConnector.MySqlSslMode.None;
|
||||
|
||||
[Browsable(false)]
|
||||
[JsonIgnore]
|
||||
public string ConnectionString
|
||||
{
|
||||
get
|
||||
{
|
||||
var builder = new MySqlConnector.MySqlConnectionStringBuilder
|
||||
{
|
||||
Server = DbServer?.Trim(),
|
||||
Database = DbName?.Trim(),
|
||||
UserID = DbUser?.Trim(),
|
||||
Password = DbPassword?.Trim(),
|
||||
AllowPublicKeyRetrieval = true,
|
||||
SslMode = DbSslMode,
|
||||
Pooling = true,
|
||||
MinimumPoolSize = 0,
|
||||
MaximumPoolSize = 100
|
||||
};
|
||||
return builder.ConnectionString;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Kestrel-Bind-URL aus Host und Port.</summary>
|
||||
[Browsable(false)]
|
||||
[JsonIgnore]
|
||||
public string WebserverUrl =>
|
||||
$"http://{(string.IsNullOrWhiteSpace(WebserverHost) ? "localhost" : WebserverHost.Trim())}:{WebserverPort}";
|
||||
|
||||
// ─── Persistenz ───
|
||||
|
||||
/// <summary>
|
||||
/// Ablageort der Einstellungen: %APPDATA%\Predictalytics unter Windows,
|
||||
/// ~/.config/Predictalytics unter Linux. Bewusst nicht neben der Programmdatei —
|
||||
/// deren Verzeichnis ist unter Linux ueblicherweise nicht beschreibbar.
|
||||
/// </summary>
|
||||
public static string SettingsDirectory => Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"Predictalytics");
|
||||
|
||||
public static string SettingsFilePath => Path.Combine(SettingsDirectory, FileName);
|
||||
|
||||
/// <summary>Alter Ablageort neben der Programmdatei (bis einschliesslich der WinForms-Fassung).</summary>
|
||||
private static string LegacySettingsFilePath => Path.Combine(AppContext.BaseDirectory, FileName);
|
||||
|
||||
public static PredictalyticsOptions Load()
|
||||
{
|
||||
// Neuer Ort hat Vorrang; sonst einmalig aus dem alten uebernehmen, damit
|
||||
// bestehende Zugangsdaten beim Umstieg nicht verloren gehen.
|
||||
var path = File.Exists(SettingsFilePath) ? SettingsFilePath
|
||||
: File.Exists(LegacySettingsFilePath) ? LegacySettingsFilePath
|
||||
: null;
|
||||
|
||||
if (path == null)
|
||||
{
|
||||
var fresh = new PredictalyticsOptions();
|
||||
fresh.Save();
|
||||
return fresh;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var options = JsonSerializer.Deserialize<PredictalyticsOptions>(File.ReadAllText(path))
|
||||
?? new PredictalyticsOptions();
|
||||
if (path == LegacySettingsFilePath) options.Save(); // Migration festschreiben
|
||||
return options;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new PredictalyticsOptions();
|
||||
}
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
Directory.CreateDirectory(SettingsDirectory);
|
||||
File.WriteAllText(SettingsFilePath,
|
||||
JsonSerializer.Serialize(this, new JsonSerializerOptions { WriteIndented = true }));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user