using ClawdDotNet.Core.Config; using ClawdDotNet.Core.Scheduling; using Microsoft.Extensions.Logging; namespace ClawdDotNet.Core.Tasks; /// /// Überführt die alten Scheduler-Konfigurationen ins Taskboard — der letzte Schritt, um /// die Alt-Scheduler abzulösen: alles Periodische ist danach ein Task. /// /// - Ein scheduler (Agent nach Cron) → ein Task mit when: cron und Assignee /// @new:<agent> (frischer Lauf, wie der alte AgentScheduler). /// - Jeder toolJobs-Eintrag (Poll) → ein Task vom Typ tool_job. /// /// Einmalig und nicht-destruktiv: Existiert der Task (stabile Id) schon, wird er /// übersprungen — spätere Änderungen an der Task-Datei bleiben erhalten. /// public sealed class SchedulerTaskMigration { private readonly TaskboardService _board; private readonly ITaskRepository _repo; private readonly ILogger _logger; public SchedulerTaskMigration(TaskboardService board, ITaskRepository repo, ILoggerFactory loggerFactory) { _board = board; _repo = repo; _logger = loggerFactory.CreateLogger("ClawdDotNet.Core.Tasks.SchedulerMigration"); } public async Task RunAsync(IEnumerable agents, CancellationToken ct) { // IANA-Schreibweise, nicht TimeZoneInfo.Local.Id: Unter Windows lieferte das // "W. Europe Standard Time", unter Linux "Europe/Berlin". Die erzeugten // Task-Dateien wandern zwischen Rechnern — sie brauchen die Form, die überall // gilt. var localTz = TimeZones.LocalIanaId; var created = 0; foreach (var agent in agents) { if (agent.Scheduler is { } scheduler && !string.IsNullOrWhiteSpace(scheduler.Cron)) created += await MigrateSchedulerAsync(agent, scheduler, localTz, ct); foreach (var job in agent.ToolJobs) created += await MigrateToolJobAsync(agent, job, localTz, ct); } if (created > 0) _logger.LogInformation("Scheduler-Migration: {Count} Task(s) aus Alt-Konfiguration angelegt", created); return created; } private async Task MigrateSchedulerAsync( AgentConfig agent, SchedulerConfig scheduler, string tz, CancellationToken ct) { var id = $"sched-{agent.AgentId}"; if (await _repo.GetAsync(id, ct) is not null) return 0; await _board.CreateAsync(new TaskItem { Id = id, Title = $"Geplanter Lauf: {Display(agent)}", Body = scheduler.TaskMessage, Status = TaskItemStatus.Todo, Type = TaskItemType.Work, Assignee = $"@new:{agent.AgentId}", // frischer Lauf, wie der alte AgentScheduler When = new TaskWhen { Kind = TaskWhenKind.Cron, Value = scheduler.Cron, TimeZone = tz } }, ct); return 1; } private async Task MigrateToolJobAsync( AgentConfig agent, ToolJobConfig job, string tz, CancellationToken ct) { if (string.IsNullOrWhiteSpace(job.Cron) || string.IsNullOrWhiteSpace(job.ToolName)) return 0; var id = $"tj-{agent.AgentId}-{job.JobId}"; if (await _repo.GetAsync(id, ct) is not null) return 0; await _board.CreateAsync(new TaskItem { Id = id, Title = $"Poll: {job.ToolName}/{job.JobTypeId} → {Display(agent)}", Body = $"Wiederkehrender {job.ToolName}-Poll ({job.JobTypeId}). Weckt {Display(agent)} bei einem Ereignis.", // Deaktivierte Jobs kommen als backlog — der Scanner nimmt nur todo/backlog... // backlog wird aber nicht geclaimt, also ruht der Poll, bis jemand ihn aktiviert. Status = job.Enabled ? TaskItemStatus.Todo : TaskItemStatus.Backlog, Type = TaskItemType.ToolJob, ToolName = job.ToolName, JobTypeId = job.JobTypeId, Assignee = $"@{agent.AgentId}", // Zielagent zum Wecken When = new TaskWhen { Kind = TaskWhenKind.Cron, Value = job.Cron, TimeZone = tz } }, ct); return 1; } private static string Display(AgentConfig agent) => string.IsNullOrWhiteSpace(agent.DisplayName) ? agent.AgentId : agent.DisplayName; }