UI-Entwurf umgesetzt, Rocket.Chat-Tool, Deploymentcenter 2.4

Sicherungspunkt vor dem Aufraeumen. Buendelt die Arbeit, die seit dem
Abschluss der Avalonia-Portierung im Arbeitsverzeichnis lag.

Oberflaeche
- Entwurf aus Mockup/ umgesetzt: Theme.axaml (Farben je Thema, Barlow als
  mitgelieferte Schrift), Icons.axaml (Symbolgeometrien), Shell.axaml
  (eigene ControlThemes statt Fluent umzufaerben).
- Neue Steuerelemente StrokeIcon und BlueprintFrame, Seiten fuer
  Token-Verbrauch und Agenten-Chats, Werkzeug-Einstellungen als Seite
  statt eigenem Fenster, Texteditor-Fenster.
- ThemeManager mit hellem und dunklem Thema; die beiden Pinsel-Konverter
  entfallen, weil ein fester Farbwert den Themenwechsel nicht ueberlebt.

Rocket.Chat
- Neues Tool-Projekt (Client, Konfiguration, Workspace-Dateien) nach der
  Bauform des Telegram-Tools: rocketchat_poll als Tool-Job, geweckt wird
  nur, wenn wirklich etwas anliegt.
- send_file ist freigabepflichtig, send_message bewusst nicht: Der Raum
  ist Arbeitsraum, der Schutz sitzt an der Raum-Allowlist.
- Konzept-Doc um die Messung gegen die echte Instanz 8.7 ergaenzt; drei
  Annahmen waren falsch und sind korrigiert.

Deploymentcenter
- DC6 (Update anwenden) und DC7 (Erstinstallation ueber setup.json)
  erledigt, DC3 fuer win-x64/dev; deploy/publish.py als Release-Strecke.
- AppHost.DisposeAsync gegen doppeltes Herunterfahren gesperrt - sonst
  ueberschreibt eine zweite Abmeldung den Wartungszustand am Watchdog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-08-23 12:16:05 +02:00
co-authored by Claude Opus 5
parent b5bf97ae74
commit 33d95a6c3f
88 changed files with 11665 additions and 1029 deletions
+229 -74
View File
@@ -3,100 +3,255 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:ClawdDotNet.Desktop.ViewModels"
mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="700"
Padding="16"
xmlns:models="using:ClawdDotNet.App.Models"
xmlns:ctrl="using:ClawdDotNet.Desktop.Controls"
xmlns:conv="using:ClawdDotNet.Desktop.Converters"
mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="740"
x:Class="ClawdDotNet.Desktop.Views.TasksPageView"
x:DataType="vm:TasksPageViewModel">
<DockPanel>
<!-- Statusbar unten -->
<Border Classes="statusbar" DockPanel.Dock="Bottom">
<TextBlock Text="{Binding StatusText}" VerticalAlignment="Center" />
</Border>
<!-- Segmentleiste statt Registerkarten. Die Knöpfe überlappen sich um einen
Bildpunkt, damit zwischen ihnen nur eine Linie steht und nicht zwei. -->
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal"
HorizontalAlignment="Left" Margin="0,0,0,14">
<ToggleButton Content="Geplante Jobs" Margin="0,0,-1,0"
IsChecked="{Binding IsJobsTab, Mode=OneWay}"
Command="{Binding SelectTabCommand}" CommandParameter="Jobs" />
<ToggleButton Content="Dienste" Margin="0,0,-1,0"
IsChecked="{Binding IsServicesTab, Mode=OneWay}"
Command="{Binding SelectTabCommand}" CommandParameter="Services" />
<ToggleButton Content="Verlauf"
IsChecked="{Binding IsHistoryTab, Mode=OneWay}"
Command="{Binding SelectTabCommand}" CommandParameter="History" />
</StackPanel>
<TabControl>
<!-- Statuszeile des Bereichs -->
<TextBlock DockPanel.Dock="Bottom" Classes="caption" Margin="0,10,0,0"
Text="{Binding StatusText}" TextWrapping="Wrap" />
<!-- TAB 1: Jobs (Scheduled / Worker) -->
<TabItem Header="Geplante Jobs">
<DockPanel Margin="0,8,0,0">
<Panel>
<!-- Toolbar -->
<StackPanel Classes="toolbar" DockPanel.Dock="Top">
<Button Content="Job hinzufügen" Command="{Binding AddJobCommand}" Classes="accent" />
<Button Content="Bearbeiten" Command="{Binding EditJobCommand}" IsEnabled="{Binding SelectedJob, Converter={x:Static ObjectConverters.IsNotNull}}" />
<Button Content="Entfernen" Command="{Binding RemoveJobCommand}" IsEnabled="{Binding SelectedJob, Converter={x:Static ObjectConverters.IsNotNull}}" />
<Button Content="▶ Jetzt ausführen" Command="{Binding RunJobNowCommand}" IsEnabled="{Binding SelectedJob, Converter={x:Static ObjectConverters.IsNotNull}}" Margin="12,0,0,0" />
<Button Content="Aktualisieren" Command="{Binding RefreshJobListCommand}" />
</StackPanel>
<!-- ══════════════════ Geplante Jobs ══════════════════ -->
<DockPanel IsVisible="{Binding IsJobsTab}">
<!-- Jobs DataGrid -->
<DataGrid ItemsSource="{Binding JobEntries}" SelectedItem="{Binding SelectedJob}" AutoGenerateColumns="False" IsReadOnly="True" GridLinesVisibility="Horizontal">
<DataGrid.Columns>
<DataGridTextColumn Header="Typ" Binding="{Binding JobType}" Width="120" />
<DataGridTextColumn Header="Agent" Binding="{Binding AgentName}" Width="150" />
<DataGridTextColumn Header="Tool / Nachricht" Binding="{Binding TaskMessage}" Width="*" />
<DataGridTextColumn Header="Cron-Ausdruck" Binding="{Binding CronExpression}" Width="120" />
<DataGridTextColumn Header="Nächster Lauf" Binding="{Binding NextRun}" Width="140" />
<DataGridTextColumn Header="Letzter Lauf" Binding="{Binding LastRun}" Width="140" />
<DataGridTextColumn Header="Status" Binding="{Binding Status}" Width="100" />
</DataGrid.Columns>
</DataGrid>
<WrapPanel DockPanel.Dock="Top" Classes="toolbar">
<Button Classes="toolbar primary" Command="{Binding AddJobCommand}">
<StackPanel Orientation="Horizontal" Spacing="6">
<ctrl:StrokeIcon Data="{StaticResource IconPlus}" Width="14" Height="14" />
<TextBlock Text="Job hinzufügen" />
</StackPanel>
</Button>
</DockPanel>
</TabItem>
<Button Classes="toolbar" Command="{Binding EditJobCommand}"
IsEnabled="{Binding SelectedJob, Converter={x:Static ObjectConverters.IsNotNull}}">
<StackPanel Orientation="Horizontal" Spacing="6">
<ctrl:StrokeIcon Data="{StaticResource IconPencil}" Width="14" Height="14" />
<TextBlock Text="Bearbeiten" />
</StackPanel>
</Button>
<!-- TAB 2: Dienste (Services) -->
<TabItem Header="Dienste (Services)">
<DockPanel Margin="0,8,0,0">
<Button Classes="toolbar" Command="{Binding RemoveJobCommand}"
IsEnabled="{Binding SelectedJob, Converter={x:Static ObjectConverters.IsNotNull}}">
<StackPanel Orientation="Horizontal" Spacing="6">
<ctrl:StrokeIcon Data="{StaticResource IconTrash}" Width="14" Height="14" />
<TextBlock Text="Entfernen" />
</StackPanel>
</Button>
<!-- Toolbar -->
<StackPanel Classes="toolbar" DockPanel.Dock="Top">
<Button Content="Dienst hinzufügen" Command="{Binding AddServiceCommand}" Classes="accent" />
<Button Content="Entfernen" Command="{Binding RemoveServiceCommand}" IsEnabled="{Binding SelectedService, Converter={x:Static ObjectConverters.IsNotNull}}" />
<Button Content="Aktualisieren" Command="{Binding RefreshServiceListCommand}" />
</StackPanel>
<Button Classes="toolbar" Command="{Binding ToggleJobEnabledCommand}"
IsEnabled="{Binding SelectedJob, Converter={x:Static ObjectConverters.IsNotNull}}">
<StackPanel Orientation="Horizontal" Spacing="6">
<ctrl:StrokeIcon Data="{StaticResource IconPower}" Width="14" Height="14" />
<TextBlock Text="Aktivieren/Deaktivieren" />
</StackPanel>
</Button>
<!-- Services DataGrid -->
<DataGrid ItemsSource="{Binding ServiceEntries}" SelectedItem="{Binding SelectedService}" AutoGenerateColumns="False" IsReadOnly="True" GridLinesVisibility="Horizontal">
<DataGrid.Columns>
<DataGridTextColumn Header="Dienstname" Binding="{Binding Name}" Width="180" />
<DataGridTextColumn Header="Typ" Binding="{Binding Type}" Width="100" />
<DataGridTextColumn Header="Port" Binding="{Binding Port}" Width="80" />
<DataGridTextColumn Header="Status" Binding="{Binding Status}" Width="110" />
<DataGridTextColumn Header="Beschreibung" Binding="{Binding Description}" Width="*" />
</DataGrid.Columns>
</DataGrid>
<Border Classes="sep" />
</DockPanel>
</TabItem>
<Button Classes="toolbar" Command="{Binding RunJobNowCommand}"
IsEnabled="{Binding SelectedJob, Converter={x:Static ObjectConverters.IsNotNull}}">
<StackPanel Orientation="Horizontal" Spacing="6">
<ctrl:StrokeIcon Data="{StaticResource IconPlay}" Width="12" Height="12" IsFilled="True" />
<TextBlock Text="Jetzt ausführen" />
</StackPanel>
</Button>
<!-- TAB 3: Verlauf (Job History) -->
<TabItem Header="Verlauf (History)">
<DockPanel Margin="0,8,0,0">
<Button Classes="toolbar" Command="{Binding RefreshJobListCommand}">
<StackPanel Orientation="Horizontal" Spacing="6">
<ctrl:StrokeIcon Data="{StaticResource IconRefresh}" Width="14" Height="14" />
<TextBlock Text="Aktualisieren" />
</StackPanel>
</Button>
</WrapPanel>
<!-- Toolbar -->
<StackPanel Classes="toolbar" DockPanel.Dock="Top">
<Button Content="Verlauf aktualisieren" Command="{Binding RefreshJobHistoryCommand}" />
</StackPanel>
<Border DockPanel.Dock="Top" Classes="thead">
<Grid ColumnDefinitions="1.1*,1.3*,2.2*,1.1*,1.3*,1.3*,0.9*">
<TextBlock Grid.Column="0" Classes="th" Text="TYP" />
<TextBlock Grid.Column="1" Classes="th" Text="AGENT" />
<TextBlock Grid.Column="2" Classes="th" Text="TOOL / NACHRICHT" />
<TextBlock Grid.Column="3" Classes="th" Text="CRON" />
<TextBlock Grid.Column="4" Classes="th" Text="NÄCHSTER LAUF" />
<TextBlock Grid.Column="5" Classes="th" Text="LETZTER LAUF" />
<TextBlock Grid.Column="6" Classes="th" Text="STATUS" />
</Grid>
</Border>
<!-- History DataGrid -->
<DataGrid ItemsSource="{Binding JobHistoryEntries}" AutoGenerateColumns="False" IsReadOnly="True" GridLinesVisibility="Horizontal">
<DataGrid.Columns>
<DataGridTextColumn Header="Zeitpunkt" Binding="{Binding Time, StringFormat='{}{0:g}'}" Width="140" />
<DataGridTextColumn Header="Job-Name" Binding="{Binding JobName}" Width="140" />
<DataGridTextColumn Header="Agent" Binding="{Binding Agent}" Width="140" />
<DataGridTextColumn Header="Beschreibung" Binding="{Binding JobDescription}" Width="220" />
<DataGridTextColumn Header="Status" Binding="{Binding Status}" Width="100" />
<DataGridTextColumn Header="Details / Info" Binding="{Binding Info}" Width="*" />
</DataGrid.Columns>
</DataGrid>
<ListBox Classes="table" ItemsSource="{Binding JobEntries}"
SelectedItem="{Binding SelectedJob}">
<ListBox.ItemTemplate>
<DataTemplate DataType="vm:JobDisplayItemViewModel">
<Border Classes="tr" Padding="7">
<Grid ColumnDefinitions="1.1*,1.3*,2.2*,1.1*,1.3*,1.3*,0.9*">
<TextBlock Grid.Column="0" Classes="td" Text="{Binding JobType}" />
<TextBlock Grid.Column="1" Classes="td" Text="{Binding AgentName}" />
<TextBlock Grid.Column="2" Classes="td" Text="{Binding TaskMessage}" />
<TextBlock Grid.Column="3" Classes="td mono" Text="{Binding CronExpression}" />
<TextBlock Grid.Column="4" Classes="td" Text="{Binding NextRun}" />
<TextBlock Grid.Column="5" Classes="td" Text="{Binding LastRun}" />
</DockPanel>
</TabItem>
<Border Grid.Column="6" Classes="tag"
Classes.ok="{Binding Status,
Converter={x:Static conv:StringEqualsConverter.Instance},
ConverterParameter=Aktiv}">
<TextBlock Text="{Binding Status}" />
</Border>
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</TabControl>
<!-- ══════════════════ Dienste ══════════════════ -->
<DockPanel IsVisible="{Binding IsServicesTab}">
<WrapPanel DockPanel.Dock="Top" Classes="toolbar">
<Button Classes="toolbar primary" Command="{Binding AddServiceCommand}">
<StackPanel Orientation="Horizontal" Spacing="6">
<ctrl:StrokeIcon Data="{StaticResource IconPlus}" Width="14" Height="14" />
<TextBlock Text="Dienst hinzufügen" />
</StackPanel>
</Button>
<Button Classes="toolbar" Command="{Binding RemoveServiceCommand}"
IsEnabled="{Binding SelectedService, Converter={x:Static ObjectConverters.IsNotNull}}">
<StackPanel Orientation="Horizontal" Spacing="6">
<ctrl:StrokeIcon Data="{StaticResource IconTrash}" Width="14" Height="14" />
<TextBlock Text="Entfernen" />
</StackPanel>
</Button>
<Border Classes="sep" />
<!-- Der Entwurf zeigt hier „Start"/„Stop". Umgelegt wird aber das Kennzeichen
in der Konfiguration, nicht ein laufender Prozess — deshalb die ehrlichere
Beschriftung. -->
<Button Classes="toolbar" Command="{Binding ToggleServiceEnabledCommand}"
IsEnabled="{Binding SelectedService, Converter={x:Static ObjectConverters.IsNotNull}}">
<StackPanel Orientation="Horizontal" Spacing="6">
<ctrl:StrokeIcon Data="{StaticResource IconPower}" Width="14" Height="14" />
<TextBlock Text="Aktivieren/Deaktivieren" />
</StackPanel>
</Button>
<Button Classes="toolbar" Command="{Binding RefreshServiceListCommand}">
<StackPanel Orientation="Horizontal" Spacing="6">
<ctrl:StrokeIcon Data="{StaticResource IconRefresh}" Width="14" Height="14" />
<TextBlock Text="Aktualisieren" />
</StackPanel>
</Button>
</WrapPanel>
<Border DockPanel.Dock="Top" Classes="thead">
<Grid ColumnDefinitions="1.8*,1*,0.7*,1*,3*">
<TextBlock Grid.Column="0" Classes="th" Text="DIENSTNAME" />
<TextBlock Grid.Column="1" Classes="th" Text="TYP" />
<TextBlock Grid.Column="2" Classes="th" Text="PORT" />
<TextBlock Grid.Column="3" Classes="th" Text="STATUS" />
<TextBlock Grid.Column="4" Classes="th" Text="BESCHREIBUNG" />
</Grid>
</Border>
<ListBox Classes="table" ItemsSource="{Binding ServiceEntries}"
SelectedItem="{Binding SelectedService}">
<ListBox.ItemTemplate>
<DataTemplate DataType="vm:ServiceDisplayItemViewModel">
<Border Classes="tr" Padding="7">
<Grid ColumnDefinitions="1.8*,1*,0.7*,1*,3*">
<TextBlock Grid.Column="0" Classes="td" FontWeight="SemiBold" Text="{Binding Name}" />
<TextBlock Grid.Column="1" Classes="td" Text="{Binding Type}" />
<TextBlock Grid.Column="2" Classes="td" Text="{Binding Port}" />
<Border Grid.Column="3" Classes="tag"
Classes.ok="{Binding Status,
Converter={x:Static conv:StringEqualsConverter.Instance},
ConverterParameter=Bereit}">
<TextBlock Text="{Binding Status}" />
</Border>
<TextBlock Grid.Column="4" Classes="td dim" Text="{Binding Description}" />
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
<!-- ══════════════════ Verlauf ══════════════════ -->
<DockPanel IsVisible="{Binding IsHistoryTab}">
<StackPanel DockPanel.Dock="Top" Classes="toolbar">
<Button Classes="toolbar" Command="{Binding RefreshJobHistoryCommand}">
<StackPanel Orientation="Horizontal" Spacing="6">
<ctrl:StrokeIcon Data="{StaticResource IconRefresh}" Width="14" Height="14" />
<TextBlock Text="Verlauf aktualisieren" />
</StackPanel>
</Button>
</StackPanel>
<Border DockPanel.Dock="Top" Classes="thead">
<Grid ColumnDefinitions="1.3*,1.6*,1.4*,2*,0.9*,2.4*">
<TextBlock Grid.Column="0" Classes="th" Text="ZEITPUNKT" />
<TextBlock Grid.Column="1" Classes="th" Text="JOB" />
<TextBlock Grid.Column="2" Classes="th" Text="AGENT" />
<TextBlock Grid.Column="3" Classes="th" Text="BESCHREIBUNG" />
<TextBlock Grid.Column="4" Classes="th" Text="STATUS" />
<TextBlock Grid.Column="5" Classes="th" Text="DETAILS" />
</Grid>
</Border>
<ItemsControl ItemsSource="{Binding JobHistoryEntries}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="models:JobHistoryEntry">
<Border Classes="tr" Padding="7">
<Grid ColumnDefinitions="1.3*,1.6*,1.4*,2*,0.9*,2.4*">
<TextBlock Grid.Column="0" Classes="td"
Text="{Binding Time, StringFormat='{}{0:dd.MM. HH:mm}'}" />
<TextBlock Grid.Column="1" Classes="td" Text="{Binding JobName}" />
<TextBlock Grid.Column="2" Classes="td" Text="{Binding Agent}" />
<TextBlock Grid.Column="3" Classes="td dim" Text="{Binding JobDescription}" />
<Border Grid.Column="4" Classes="tag"
Classes.ok="{Binding Status,
Converter={x:Static conv:StringEqualsConverter.Instance},
ConverterParameter=Success}"
Classes.bad="{Binding Status,
Converter={x:Static conv:StringEqualsConverter.Instance},
ConverterParameter=Error}">
<TextBlock Text="{Binding Status}" />
</Border>
<TextBlock Grid.Column="5" Classes="td dim" Text="{Binding Info}" />
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DockPanel>
</Panel>
</DockPanel>
</UserControl>