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
@@ -26,6 +26,14 @@ public sealed record JobDisplayItemViewModel(
string Status
);
/// <summary>Die drei Ansichten der Aufgaben-Seite.</summary>
public enum TasksTab
{
Jobs,
Services,
History
}
public sealed record ServiceDisplayItemViewModel(
string ServiceId,
string Name,
@@ -61,6 +69,22 @@ public sealed partial class TasksPageViewModel : PageViewModel
[ObservableProperty]
private bool _isRunningJob;
/// <summary>
/// Der Entwurf ersetzt die Registerkarten durch eine Segmentleiste. Fachlich ist das
/// dasselbe: eine von drei Ansichten. Die drei Kennzeichen darunter gibt es, weil
/// eine Bindung „Ist dieser Wert gleich Services?" in AXAML sonst jedes Mal einen
/// Konverter mit Parameter bräuchte.
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsJobsTab))]
[NotifyPropertyChangedFor(nameof(IsServicesTab))]
[NotifyPropertyChangedFor(nameof(IsHistoryTab))]
private TasksTab _selectedTab = TasksTab.Jobs;
public bool IsJobsTab => SelectedTab == TasksTab.Jobs;
public bool IsServicesTab => SelectedTab == TasksTab.Services;
public bool IsHistoryTab => SelectedTab == TasksTab.History;
public event Func<AddJobViewModel, Task<AddJobResultData?>>? RequestAddJobDialog;
public event Func<AddServiceViewModel, Task<AddServiceResultData?>>? RequestAddServiceDialog;
@@ -429,6 +453,85 @@ public sealed partial class TasksPageViewModel : PageViewModel
RefreshServiceList();
}
/// <summary>
/// Schaltet einen Job an oder ab, ohne ihn zu entfernen — im Entwurf die neue
/// Schaltfläche „Aktivieren/Deaktivieren" in der Werkzeugleiste.
///
/// <para>Gilt nur für Tool-Jobs: <c>SchedulerConfig</c> hat keinen solchen Schalter,
/// ein Agent-Wakeup existiert oder existiert nicht. Ihn hier zu ergänzen wäre keine
/// Oberflächenarbeit — der Scanner müsste den Schalter erst einmal lesen, sonst liefe
/// ein „deaktivierter" Wakeup munter weiter.</para>
/// </summary>
[RelayCommand]
private void ToggleJobEnabled()
{
if (_host is null || SelectedJob is null) return;
var entry = SelectedJob;
if (entry.JobType != "Tool Job")
{
StatusText = "Nur Tool-Jobs lassen sich abschalten. "
+ "Ein Agent-Wakeup wird über die Agenten-Seite entfernt.";
return;
}
var agent = _host.Instance.Agents.FirstOrDefault(a => a.AgentId == entry.AgentId);
var job = agent?.ToolJobs.FirstOrDefault(j => j.JobId == entry.JobId);
if (job is null) return;
job.Enabled = !job.Enabled;
SaveInstanceConfig();
RefreshJobList();
// Die Auswahl geht beim Neuaufbau der Liste verloren — sie wieder zu setzen
// erspart es, für zwei Schaltvorgänge zweimal dieselbe Zeile anzuklicken.
SelectedJob = JobEntries.FirstOrDefault(j => j.JobId == entry.JobId);
StatusText = job.Enabled
? $"Job '{entry.TaskMessage}' aktiviert."
: $"Job '{entry.TaskMessage}' deaktiviert.";
}
/// <summary>
/// Aktiviert bzw. deaktiviert einen Dienst.
///
/// <para>Bewusst nicht „Start"/„Stop" genannt, auch wenn der Entwurf das so zeigt:
/// Umgelegt wird das Kennzeichen <c>Enabled</c> in der Instanzkonfiguration. Ein
/// laufender Dienstprozess wird davon nicht angehalten — eine Schaltfläche „Stop",
/// nach der etwas weiterläuft, wäre schlimmer als gar keine.</para>
/// </summary>
[RelayCommand]
private void ToggleServiceEnabled()
{
if (_host is null || SelectedService is null) return;
var id = SelectedService.ServiceId;
var service = _host.Instance.Services.FirstOrDefault(s => s.ServiceId == id);
if (service is null) return;
service.Enabled = !service.Enabled;
SaveInstanceConfig();
RefreshServiceList();
SelectedService = ServiceEntries.FirstOrDefault(s => s.ServiceId == id);
StatusText = service.Enabled
? $"Dienst '{service.Name}' aktiviert."
: $"Dienst '{service.Name}' deaktiviert.";
}
[RelayCommand]
private void SelectTab(string? tab)
{
SelectedTab = tab switch
{
"Services" => TasksTab.Services,
"History" => TasksTab.History,
_ => TasksTab.Jobs
};
}
private static string CalculateNextRun(string? cronExpr)
{
if (string.IsNullOrWhiteSpace(cronExpr)) return "—";