feat(ui): complete Avalonia UI port with 7 main pages, tool settings & top MenuBar

This commit is contained in:
Richard
2026-08-10 10:48:34 +02:00
parent a0e18d2a57
commit b5bf97ae74
187 changed files with 20054 additions and 882 deletions
@@ -0,0 +1,446 @@
using System.Collections.ObjectModel;
using Avalonia.Threading;
using ClawdDotNet.App;
using ClawdDotNet.App.Models;
using ClawdDotNet.App.Services;
using ClawdDotNet.Core.Config;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ClawdDotNet.Core.Scheduling;
using Microsoft.Extensions.Logging;
namespace ClawdDotNet.Desktop.ViewModels;
public sealed record JobDisplayItemViewModel(
string JobType,
string JobId,
string AgentId,
string AgentName,
string ToolName,
string CronExpression,
string TaskMessage,
bool RunOnStart,
string NextRun,
string LastRun,
string LastStatus,
string Status
);
public sealed record ServiceDisplayItemViewModel(
string ServiceId,
string Name,
string Type,
int Port,
string Status,
string Description
);
/// <summary>
/// Ansichtsmodell für die Aufgaben-Seite (Jobs, Dienste, Verlauf).
/// </summary>
public sealed partial class TasksPageViewModel : PageViewModel
{
private readonly AppHost? _host;
private readonly JobHistoryService? _jobHistoryService;
private readonly ILogger? _logger;
private readonly Dictionary<string, DateTime> _jobLastRunTimes = new();
public ObservableCollection<JobDisplayItemViewModel> JobEntries { get; } = [];
public ObservableCollection<ServiceDisplayItemViewModel> ServiceEntries { get; } = [];
public ObservableCollection<JobHistoryEntry> JobHistoryEntries { get; } = [];
[ObservableProperty]
private JobDisplayItemViewModel? _selectedJob;
[ObservableProperty]
private ServiceDisplayItemViewModel? _selectedService;
[ObservableProperty]
private string _statusText = "Bereit";
[ObservableProperty]
private bool _isRunningJob;
public event Func<AddJobViewModel, Task<AddJobResultData?>>? RequestAddJobDialog;
public event Func<AddServiceViewModel, Task<AddServiceResultData?>>? RequestAddServiceDialog;
public TasksPageViewModel() : this(null) { }
public TasksPageViewModel(AppHost? host) : base("Aufgaben")
{
_host = host;
if (host is not null)
{
_jobHistoryService = new JobHistoryService(host.InstancePath);
_logger = host.LoggerFactory.CreateLogger("ClawdDotNet.Desktop.Tasks");
EnsureBuiltInServices();
RefreshJobList();
RefreshServiceList();
RefreshJobHistory();
}
else
{
StatusText = "Entwurfsmodus";
}
}
private void SaveInstanceConfig()
{
if (_host is null) return;
_host.Directories.SaveInstanceConfig(_host.InstancePath, _host.Instance);
}
private void EnsureBuiltInServices()
{
if (_host is null) return;
var defaults = BuiltInServices.CreateDefaults();
var changed = false;
foreach (var def in defaults)
{
if (_host.Instance.Services.Any(s => s.ServiceId == def.ServiceId))
continue;
_host.Instance.Services.Insert(0, def);
changed = true;
}
if (changed)
SaveInstanceConfig();
}
[RelayCommand]
private void RefreshJobList()
{
JobEntries.Clear();
if (_host is null) return;
foreach (var agent in _host.Instance.Agents)
{
// Agent Wakeup Jobs
if (agent.Scheduler is not null)
{
var nextRun = CalculateNextRun(agent.Scheduler.Cron);
var agentLastRun = _jobLastRunTimes.TryGetValue($"agent_{agent.AgentId}", out var agentLastTime)
? agentLastTime.ToString("yyyy-MM-dd HH:mm:ss")
: "—";
JobEntries.Add(new JobDisplayItemViewModel(
"Agent Wakeup",
$"agent_{agent.AgentId}",
agent.AgentId,
agent.DisplayName,
"—",
agent.Scheduler.Cron,
agent.Scheduler.TaskMessage,
agent.Scheduler.RunOnStart,
nextRun,
agentLastRun,
"—",
_host.Scanner is not null ? "Aktiv" : "Inaktiv"
));
}
// Tool Jobs
foreach (var toolJob in agent.ToolJobs)
{
var nextRun = CalculateNextRun(toolJob.Cron);
var toolLastRun = _jobLastRunTimes.TryGetValue(toolJob.JobId, out var lastTime)
? lastTime.ToString("yyyy-MM-dd HH:mm:ss")
: "—";
JobEntries.Add(new JobDisplayItemViewModel(
"Tool Job",
toolJob.JobId,
agent.AgentId,
agent.DisplayName,
toolJob.ToolName,
toolJob.Cron,
toolJob.JobTypeId,
toolJob.RunOnStart,
nextRun,
toolLastRun,
"—",
toolJob.Enabled ? "Aktiv" : "Deaktiviert"
));
}
}
}
[RelayCommand]
private void RefreshServiceList()
{
ServiceEntries.Clear();
if (_host is null) return;
foreach (var svc in _host.Instance.Services)
{
ServiceEntries.Add(new ServiceDisplayItemViewModel(
svc.ServiceId,
svc.Name,
svc.Type,
svc.Port,
svc.Enabled ? "Bereit" : "Deaktiviert",
svc.Description
));
}
}
[RelayCommand]
private void RefreshJobHistory()
{
JobHistoryEntries.Clear();
if (_jobHistoryService is null) return;
foreach (var entry in _jobHistoryService.GetAll().Take(200))
JobHistoryEntries.Add(entry);
}
[RelayCommand]
private async Task AddJobAsync()
{
if (_host is null || RequestAddJobDialog is null) return;
var vm = new AddJobViewModel(_host.Instance.Agents);
var result = await RequestAddJobDialog(vm);
if (result is null) return;
var agent = _host.Instance.Agents.FirstOrDefault(a => a.AgentId == result.AgentId);
if (agent is null) return;
if (result.IsToolJob)
{
agent.ToolJobs.Add(new ToolJobConfig
{
ToolName = result.ToolName,
JobTypeId = result.JobTypeId,
Cron = result.CronExpression,
RunOnStart = result.RunOnStart,
Enabled = true
});
_logger?.LogInformation("Tool-Job hinzugefügt: Agent={Agent}, Tool={Tool}, Cron={Cron}",
agent.DisplayName, result.ToolName, result.CronExpression);
}
else
{
agent.Scheduler = new SchedulerConfig
{
Cron = result.CronExpression,
RunOnStart = result.RunOnStart,
TaskMessage = result.TaskMessage
};
_logger?.LogInformation("Agent-Wakeup-Job hinzugefügt: Agent={Agent}, Cron={Cron}",
agent.DisplayName, result.CronExpression);
}
SaveInstanceConfig();
RefreshJobList();
}
[RelayCommand]
private async Task EditJobAsync()
{
if (_host is null || SelectedJob is null || RequestAddJobDialog is null) return;
var entry = SelectedJob;
var agent = _host.Instance.Agents.FirstOrDefault(a => a.AgentId == entry.AgentId);
if (agent is null) return;
var isToolJob = entry.JobType == "Tool Job";
var vm = new AddJobViewModel(_host.Instance.Agents)
{
SelectedAgent = agent,
IsToolJob = isToolJob,
SelectedToolName = isToolJob ? entry.ToolName : "FileRW",
JobTypeId = isToolJob ? entry.TaskMessage : "PollJob",
CronExpression = entry.CronExpression,
TaskMessage = isToolJob ? "" : entry.TaskMessage,
RunOnStart = entry.RunOnStart
};
var result = await RequestAddJobDialog(vm);
if (result is null) return;
if (isToolJob)
{
var toolJob = agent.ToolJobs.FirstOrDefault(j => j.JobId == entry.JobId);
if (toolJob is not null)
{
toolJob.Cron = result.CronExpression;
toolJob.RunOnStart = result.RunOnStart;
toolJob.JobTypeId = result.JobTypeId;
toolJob.ToolName = result.ToolName;
}
}
else
{
if (agent.Scheduler is not null)
{
agent.Scheduler.Cron = result.CronExpression;
agent.Scheduler.TaskMessage = result.TaskMessage;
agent.Scheduler.RunOnStart = result.RunOnStart;
}
}
SaveInstanceConfig();
RefreshJobList();
}
[RelayCommand]
private void RemoveJob()
{
if (_host is null || SelectedJob is null) return;
var entry = SelectedJob;
var agent = _host.Instance.Agents.FirstOrDefault(a => a.AgentId == entry.AgentId);
if (agent is null) return;
if (entry.JobType == "Tool Job")
{
agent.ToolJobs.RemoveAll(j => j.JobId == entry.JobId);
}
else
{
agent.Scheduler = null;
}
SaveInstanceConfig();
RefreshJobList();
}
[RelayCommand]
private async Task RunJobNowAsync()
{
if (_host is null || SelectedJob is null) return;
var entry = SelectedJob;
var agent = _host.Instance.Agents.FirstOrDefault(a => a.AgentId == entry.AgentId);
if (agent is null) return;
IsRunningJob = true;
StatusText = $"Führe Job für '{agent.DisplayName}' aus…";
try
{
if (entry.JobType == "Tool Job")
{
if (_host.Scanner is null)
{
StatusText = "Scanner ist nicht aktiv.";
return;
}
var jobConfig = agent.ToolJobs.FirstOrDefault(j => j.JobId == entry.JobId);
if (jobConfig is null) return;
var ran = await _host.Scanner.RunTaskNowAsync(
$"tj-{agent.AgentId}-{jobConfig.JobId}", CancellationToken.None);
_jobLastRunTimes[jobConfig.JobId] = DateTime.Now;
var info = ran ? "Ausgeführt" : "Nicht bereit";
_jobHistoryService?.Add(new JobHistoryEntry
{
JobName = jobConfig.JobTypeId,
Agent = agent.DisplayName,
Time = DateTime.Now,
JobDescription = $"Tool Job: {jobConfig.ToolName}",
Info = info,
Status = "Manual"
});
StatusText = $"Tool Job '{jobConfig.JobTypeId}' abgeschlossen ({info}).";
}
else
{
if (_host.Engine is null)
{
StatusText = "Agent-Engine ist nicht aktiv (kein API-Key).";
return;
}
var taskMessage = agent.Scheduler?.TaskMessage ?? "Führe deine zugewiesenen Aufgaben aus.";
var result = await _host.Engine.RunAsync(agent, taskMessage, _host.Instance.InstanceId, CancellationToken.None);
_jobLastRunTimes[$"agent_{agent.AgentId}"] = DateTime.Now;
_jobHistoryService?.Add(new JobHistoryEntry
{
JobName = "Agent Wakeup",
Agent = agent.DisplayName,
Time = DateTime.Now,
JobDescription = taskMessage,
Info = $"{result.Status} ({result.StepCount} Steps, {result.TokensUsed:N0} Tokens)",
Status = "Manual"
});
StatusText = $"Agent '{agent.DisplayName}' abgeschlossen: {result.Status} ({result.TokensUsed:N0} Tokens).";
}
RefreshJobList();
RefreshJobHistory();
}
catch (Exception ex)
{
StatusText = $"Fehler beim Ausführen: {ex.Message}";
_logger?.LogError(ex, "Job-Ausführung fehlgeschlagen");
}
finally
{
IsRunningJob = false;
}
}
[RelayCommand]
private async Task AddServiceAsync()
{
if (_host is null || RequestAddServiceDialog is null) return;
var vm = new AddServiceViewModel();
var result = await RequestAddServiceDialog(vm);
if (result is null) return;
var svcConfig = new ServiceConfig
{
Name = result.ServiceName,
Type = result.ServiceType,
Port = result.ServicePort,
Enabled = true,
Description = result.ServiceDescription
};
_host.Instance.Services.Add(svcConfig);
SaveInstanceConfig();
RefreshServiceList();
}
[RelayCommand]
private void RemoveService()
{
if (_host is null || SelectedService is null) return;
_host.Instance.Services.RemoveAll(s => s.ServiceId == SelectedService.ServiceId);
SaveInstanceConfig();
RefreshServiceList();
}
private static string CalculateNextRun(string? cronExpr)
{
if (string.IsNullOrWhiteSpace(cronExpr)) return "—";
try
{
var cron = CronExpression.Parse(cronExpr);
var next = cron.GetNextOccurrence(DateTime.UtcNow);
return next?.ToLocalTime().ToString("yyyy-MM-dd HH:mm") ?? "—";
}
catch
{
return "Ungültig";
}
}
}