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 ); /// Die drei Ansichten der Aufgaben-Seite. public enum TasksTab { Jobs, Services, History } public sealed record ServiceDisplayItemViewModel( string ServiceId, string Name, string Type, int Port, string Status, string Description ); /// /// Ansichtsmodell für die Aufgaben-Seite (Jobs, Dienste, Verlauf). /// public sealed partial class TasksPageViewModel : PageViewModel { private readonly AppHost? _host; private readonly JobHistoryService? _jobHistoryService; private readonly ILogger? _logger; private readonly Dictionary _jobLastRunTimes = new(); public ObservableCollection JobEntries { get; } = []; public ObservableCollection ServiceEntries { get; } = []; public ObservableCollection JobHistoryEntries { get; } = []; [ObservableProperty] private JobDisplayItemViewModel? _selectedJob; [ObservableProperty] private ServiceDisplayItemViewModel? _selectedService; [ObservableProperty] private string _statusText = "Bereit"; [ObservableProperty] private bool _isRunningJob; /// /// 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. /// [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>? RequestAddJobDialog; public event Func>? 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(); } /// /// Schaltet einen Job an oder ab, ohne ihn zu entfernen — im Entwurf die neue /// Schaltfläche „Aktivieren/Deaktivieren" in der Werkzeugleiste. /// /// Gilt nur für Tool-Jobs: SchedulerConfig 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. /// [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."; } /// /// Aktiviert bzw. deaktiviert einen Dienst. /// /// Bewusst nicht „Start"/„Stop" genannt, auch wenn der Entwurf das so zeigt: /// Umgelegt wird das Kennzeichen Enabled 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. /// [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 "—"; 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"; } } }