Files
Predictalytics/src/Predictalytics.Shell/Views/MainWindow.axaml
T
RichardandClaude Opus 5 b175df6d3e Phase 4: Steuerendpunkte absichern, Kultur und Log-Pfad plattformfest
Die schreibenden API-Endpunkte waren ungeschuetzt. /api/capabilities meldete
zwar CanControl und AuthRequired aus der Konfiguration, erzwungen wurde davon
nichts: MapPredictalyticsControlEndpoints mappte Jobs, Trader-Steuerung und
Dev-Endpunkte bedingungslos.

Neu entscheidet die Bind-Adresse:
- localhost ohne Token: aktiv wie bisher, das Betriebssystem schuetzt
- localhost mit Token: aktiv, Token wird verlangt
- extern ohne Token: Endpunkte werden gar nicht gemappt, dazu ein Log.Error
- extern mit Token: Endpunkte verlangen X-Predictalytics-Key

Bewusst fail-safe herum, damit eine unbedachte Umstellung der Bind-Adresse
nicht stillschweigend die Steuerschnittstelle oeffnet. ApiTokenFilter
vergleicht laufzeitkonstant ueber CryptographicOperations.FixedTimeEquals.
Swagger ist bei externer Bindung abgeschaltet. /api/capabilities meldet jetzt
den tatsaechlichen Zustand statt einer Konfigurationsabsicht.

Weiter:
- RuntimeSetup.UseInvariantCulture als Erstes in beiden Startpfaden, damit
  die Systemlocale nicht auf Zahlen und Zeitstempel durchschlaegt
- Logverzeichnis neben den Einstellungen statt neben der Programmdatei,
  ueber PREDICTALYTICS_LOG_DIR uebersteuerbar
- CORS erlaubt immer die eigene Bind-URL, zusaetzliche Herkuenfte weiter
  ueber appsettings.json
- fehlendes Schwester-Repo Deploymentcenter scheitert mit verstaendlicher
  Meldung samt Klon-URL statt mit einem Fehler ueber eine fehlende csproj
- EgressPoolService: Linux-Verhalten von Socket.Bind kommentiert

24 neue Tests: IsPubliclyBound fuer Loopback-Schreibweisen, Leerwerte und
externe Adressen; dass leere Eingaben bestehende DB-Zugangsdaten nicht
ueberschreiben; und der Token-Filter gegen fehlende, falsche, zu kurze,
zu lange und abweichend geschriebene Token.

Build: 0 Fehler, 8 Warnungen (alle vorbestehend).
Tests: 124 bestanden, 0 Fehler, 1 uebersprungen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 21:13:49 +02:00

259 lines
14 KiB
XML

<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Predictalytics.Shell.ViewModels"
xmlns:hosting="using:Predictalytics.Hosting"
xmlns:sql="using:MySqlConnector"
xmlns:sys="using:System"
x:Class="Predictalytics.Shell.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
Title="Predictalytics Analytics — Backend Server"
Width="1400" Height="900"
MinWidth="900" MinHeight="600"
WindowStartupLocation="CenterScreen">
<Window.Resources>
<!-- Monospace mit Fallback-Kette: Cascadia gibt es unter Linux meist nicht. -->
<FontFamily x:Key="TerminalFont">Cascadia Code,DejaVu Sans Mono,Liberation Mono,Consolas,monospace</FontFamily>
</Window.Resources>
<Window.Styles>
<Style Selector="TextBlock.label">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Margin" Value="0,0,12,0" />
</Style>
<Style Selector="TextBlock.hint">
<Setter Property="Opacity" Value="0.6" />
<Setter Property="TextWrapping" Value="Wrap" />
<Setter Property="FontSize" Value="11" />
<Setter Property="Margin" Value="0,2,0,0" />
</Style>
<Style Selector="HeaderedContentControl.group">
<Setter Property="Margin" Value="0,0,0,20" />
</Style>
</Window.Styles>
<DockPanel>
<!-- ─── Menü ─── -->
<Menu DockPanel.Dock="Top">
<MenuItem Header="_Datei">
<MenuItem Header="_Beenden" Click="OnExitClick" />
</MenuItem>
<MenuItem Header="_Ansicht">
<MenuItem Header="Log-Ordner öffnen" Command="{Binding OpenLogFolderCommand}" />
<MenuItem Header="WebUI öffnen" Command="{Binding OpenWebUiCommand}" />
</MenuItem>
<MenuItem Header="_Entwicklung">
<MenuItem Header="Märkte synchronisieren" Command="{Binding SyncMarketsCommand}" />
<MenuItem Header="Datenbank aktualisieren" Command="{Binding UpdateDatabaseCommand}" />
<Separator />
<MenuItem Header="Alle Trader neu berechnen…" Command="{Binding RecalculateAllCommand}" />
</MenuItem>
<MenuItem Header="Deployment _Center">
<MenuItem Header="Nach Updates suchen" Command="{Binding CheckForUpdatesInteractiveCommand}" />
<MenuItem Header="Lizenzstatus anzeigen" Command="{Binding ShowLicenseStatusCommand}" />
</MenuItem>
</Menu>
<!-- ─── Werkzeugleiste ─── -->
<Border DockPanel.Dock="Top" Padding="8" BorderThickness="0,0,0,1"
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Content="{Binding ServerButtonText}" Command="{Binding ToggleServerCommand}" MinWidth="150" />
<Button Content="{Binding WebserverButtonText}" Command="{Binding ToggleWebserverCommand}" MinWidth="170" />
<ProgressBar IsIndeterminate="True" Width="120" VerticalAlignment="Center"
IsVisible="{Binding IsBusy}" />
</StackPanel>
</Border>
<!-- ─── Statusleiste ─── -->
<Border DockPanel.Dock="Bottom" Padding="8,4" BorderThickness="0,1,0,0"
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}">
<Grid ColumnDefinitions="*,Auto,Auto">
<TextBlock Grid.Column="0" Text="{Binding StatusText}" VerticalAlignment="Center" />
<TextBlock Grid.Column="1" Text="{Binding DbSizeText}" Margin="16,0" VerticalAlignment="Center" />
<TextBlock Grid.Column="2" Text="{Binding BuildVersionText}" VerticalAlignment="Center" Opacity="0.7" />
</Grid>
</Border>
<!-- ─── Inhalt ─── -->
<TabControl>
<TabItem Header="Terminal">
<ListBox Name="LogList"
ItemsSource="{Binding LogLines}"
Background="#0F0F14"
FontFamily="{StaticResource TerminalFont}"
FontSize="12"
SelectionMode="Multiple">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:LogLine">
<TextBlock Text="{Binding Text}"
Foreground="{Binding Foreground}"
TextWrapping="NoWrap" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</TabItem>
<TabItem Header="Einstellungen">
<Grid RowDefinitions="*,Auto">
<ScrollViewer Grid.Row="0" Padding="20">
<StackPanel MaxWidth="760" HorizontalAlignment="Left"
DataContext="{Binding Options}" x:DataType="hosting:PredictalyticsOptions">
<!-- Webserver -->
<HeaderedContentControl Classes="group" Header="Webserver">
<StackPanel Spacing="12" Margin="0,8,0,0">
<Grid ColumnDefinitions="200,*">
<TextBlock Grid.Column="0" Classes="label" Text="Port" />
<NumericUpDown Grid.Column="1" Value="{Binding WebserverPort}"
Minimum="1" Maximum="65535" FormatString="0" HorizontalAlignment="Left" Width="160" />
</Grid>
<Grid ColumnDefinitions="200,*">
<TextBlock Grid.Column="0" Classes="label" Text="Bind-Adresse" />
<StackPanel Grid.Column="1">
<TextBox Text="{Binding WebserverHost}" Width="260" HorizontalAlignment="Left" />
<TextBlock Classes="hint"
Text="'localhost' = nur lokal erreichbar. '0.0.0.0' macht die API von außen erreichbar — dann unbedingt absichern." />
</StackPanel>
</Grid>
<Grid ColumnDefinitions="200,*">
<TextBlock Grid.Column="0" Classes="label" Text="API-Token" />
<StackPanel Grid.Column="1">
<TextBox Text="{Binding ApiControlToken}" PasswordChar="•" />
<TextBlock Classes="hint"
Text="Schützt die schreibenden Endpunkte (Jobs, Trader-Steuerung, Dev) über den Header X-Predictalytics-Key. Bei einer Bind-Adresse außerhalb von localhost Pflicht — ohne Token werden diese Endpunkte gar nicht bereitgestellt." />
</StackPanel>
</Grid>
<Grid ColumnDefinitions="200,*">
<TextBlock Grid.Column="0" Classes="label" Text="Datenbank-Debug" />
<CheckBox Grid.Column="1" IsChecked="{Binding DbConnectionDebug}"
Content="Ausführliche Verbindungsinfos im Terminal" />
</Grid>
</StackPanel>
</HeaderedContentControl>
<!-- Datenbank -->
<HeaderedContentControl Classes="group" Header="Datenbank">
<StackPanel Spacing="12" Margin="0,8,0,0">
<Grid ColumnDefinitions="200,*">
<TextBlock Grid.Column="0" Classes="label" Text="Server" />
<TextBox Grid.Column="1" Text="{Binding DbServer}" />
</Grid>
<Grid ColumnDefinitions="200,*">
<TextBlock Grid.Column="0" Classes="label" Text="Datenbank" />
<TextBox Grid.Column="1" Text="{Binding DbName}" />
</Grid>
<Grid ColumnDefinitions="200,*">
<TextBlock Grid.Column="0" Classes="label" Text="Benutzer" />
<TextBox Grid.Column="1" Text="{Binding DbUser}" />
</Grid>
<Grid ColumnDefinitions="200,*">
<TextBlock Grid.Column="0" Classes="label" Text="Passwort" />
<TextBox Grid.Column="1" Text="{Binding DbPassword}" PasswordChar="•" />
</Grid>
<Grid ColumnDefinitions="200,*">
<TextBlock Grid.Column="0" Classes="label" Text="SSL-Modus" />
<StackPanel Grid.Column="1">
<ComboBox SelectedItem="{Binding DbSslMode}" Width="200" HorizontalAlignment="Left"
ItemsSource="{x:Static vm:OptionSources.SslModes}" />
<TextBlock Classes="hint"
Text="Bei entfernten Servern 'Preferred' empfohlen. 'None' überträgt unverschlüsselt." />
</StackPanel>
</Grid>
</StackPanel>
</HeaderedContentControl>
<!-- Deployment Center -->
<HeaderedContentControl Classes="group" Header="Deployment Center">
<StackPanel Spacing="12" Margin="0,8,0,0">
<Grid ColumnDefinitions="200,*">
<TextBlock Grid.Column="0" Classes="label" Text="Server-URL" />
<StackPanel Grid.Column="1">
<SelectableTextBlock Text="{Binding DcServerUrl}" VerticalAlignment="Center" Opacity="0.8" />
<TextBlock Classes="hint"
Text="Fest einkompiliert — ein einstellbarer Endpoint würde erlauben, die App auf einen gefälschten Lizenz- oder Update-Server zu zeigen." />
</StackPanel>
</Grid>
<Grid ColumnDefinitions="200,*">
<TextBlock Grid.Column="0" Classes="label" Text="API Token" />
<StackPanel Grid.Column="1">
<TextBox Text="{Binding DcToken}" PasswordChar="•" />
<TextBlock Classes="hint"
Text="Benötigte Rechte: 'watchdog:ping' für Heartbeats, 'bugtracker:report' für das Fehler-Reporting." />
</StackPanel>
</Grid>
<Grid ColumnDefinitions="200,*">
<TextBlock Grid.Column="0" Classes="label" Text="Heartbeat aktiv" />
<CheckBox Grid.Column="1" IsChecked="{Binding DcHeartbeatEnabled}"
Content="Heartbeats senden (Dead-Man's-Switch)" />
</Grid>
<Grid ColumnDefinitions="200,*">
<TextBlock Grid.Column="0" Classes="label" Text="Monitor Source" />
<TextBox Grid.Column="1" Text="{Binding DcSource}" />
</Grid>
<Grid ColumnDefinitions="200,*">
<TextBlock Grid.Column="0" Classes="label" Text="Monitor Instance" />
<TextBox Grid.Column="1" Text="{Binding DcInstance}" />
</Grid>
<Grid ColumnDefinitions="200,*">
<TextBlock Grid.Column="0" Classes="label" Text="Intervall (Sek.)" />
<StackPanel Grid.Column="1">
<NumericUpDown Value="{Binding DcHeartbeatIntervalSeconds}"
Minimum="15" Maximum="3600" FormatString="0" HorizontalAlignment="Left" Width="160" />
<TextBlock Classes="hint"
Text="Der Evaluator stuft nach dem Doppelten auf 'warning' und nach dem Vierfachen auf 'down'." />
</StackPanel>
</Grid>
<Grid ColumnDefinitions="200,*">
<TextBlock Grid.Column="0" Classes="label" Text="Fehler melden" />
<CheckBox Grid.Column="1" IsChecked="{Binding DcErrorReportingEnabled}"
Content="Laufzeitfehler (Error/Fatal) an den Fehler-Stream melden" />
</Grid>
<Grid ColumnDefinitions="200,*">
<TextBlock Grid.Column="0" Classes="label" Text="Update-Prüfung" />
<CheckBox Grid.Column="1" IsChecked="{Binding DcUpdateCheckEnabled}"
Content="Beim Start auf neuere Releases prüfen (installiert nichts automatisch)" />
</Grid>
<Grid ColumnDefinitions="200,*">
<TextBlock Grid.Column="0" Classes="label" Text="Update-Kanal" />
<ComboBox Grid.Column="1" SelectedItem="{Binding DcUpdateChannel}" Width="200"
HorizontalAlignment="Left"
ItemsSource="{x:Static vm:OptionSources.UpdateChannels}" />
</Grid>
</StackPanel>
</HeaderedContentControl>
<!-- Egress -->
<HeaderedContentControl Classes="group" Header="Egress (Proxy / Quell-IP)">
<StackPanel Spacing="8" Margin="0,8,0,0">
<TextBlock Classes="hint"
Text="Ein Kanal pro Zeile im Format: id|type|value&#10;Beispiel: prox-1|Proxy|http://user:pass@proxy:8080&#10; ip-1|SourceIp|192.168.1.100" />
<TextBox Text="{Binding EgressChannelsText}"
AcceptsReturn="True"
TextWrapping="NoWrap"
MinHeight="120"
FontFamily="{StaticResource TerminalFont}"
FontSize="12" />
</StackPanel>
</HeaderedContentControl>
</StackPanel>
</ScrollViewer>
<Border Grid.Row="1" Padding="20,12" BorderThickness="0,1,0,0"
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}">
<StackPanel Orientation="Horizontal" Spacing="12">
<Button Content="Speichern &amp; übernehmen" Command="{Binding SaveSettingsCommand}" />
<TextBlock VerticalAlignment="Center" Opacity="0.6"
Text="Port und Datenbank wirken beim nächsten Start des Webservers." />
</StackPanel>
</Border>
</Grid>
</TabItem>
</TabControl>
</DockPanel>
</Window>