From 5842246c75b341bb20c5b21e37efdc44534c8a12 Mon Sep 17 00:00:00 2001 From: tengo-lebanidze Date: Fri, 23 Jan 2026 19:04:27 +0400 Subject: [PATCH 1/2] Add quality of life improvements to scheduler page --- .../CommandModels/Schedule/CuteSchedule.cs | 4 + .../BaseCommands/BaseServerCommand.cs | 4 +- .../Commands/Server/ServerSchedulerCommand.cs | 178 ++++++++++++++++-- 3 files changed, 167 insertions(+), 19 deletions(-) diff --git a/source/Cute.Lib/Contentful/CommandModels/Schedule/CuteSchedule.cs b/source/Cute.Lib/Contentful/CommandModels/Schedule/CuteSchedule.cs index d8667b3..69be170 100644 --- a/source/Cute.Lib/Contentful/CommandModels/Schedule/CuteSchedule.cs +++ b/source/Cute.Lib/Contentful/CommandModels/Schedule/CuteSchedule.cs @@ -4,6 +4,10 @@ namespace Cute.Lib.Contentful.CommandModels.Schedule { public class CuteSchedule : IContent { + public static readonly string RUNNING = "running"; + public static readonly string SUCCESS = "success"; + public static readonly string ERROR = "error"; + public SystemProperties Sys { get; set; } = default!; public string Id => Sys != null ? Sys.Id : ""; public string Key { get; set; } = default!; diff --git a/source/Cute/Commands/BaseCommands/BaseServerCommand.cs b/source/Cute/Commands/BaseCommands/BaseServerCommand.cs index 537412e..9eb8f2e 100644 --- a/source/Cute/Commands/BaseCommands/BaseServerCommand.cs +++ b/source/Cute/Commands/BaseCommands/BaseServerCommand.cs @@ -27,7 +27,7 @@ public override ValidationResult Validate(CommandContext context, TSettings sett private string HtmlHeader => $""" - + @@ -37,7 +37,7 @@ public override ValidationResult Validate(CommandContext context, TSettings sett {_prettifyColors} - + """; private static string HtmlFooter => $""" diff --git a/source/Cute/Commands/Server/ServerSchedulerCommand.cs b/source/Cute/Commands/Server/ServerSchedulerCommand.cs index 69cbb05..0da2f00 100644 --- a/source/Cute/Commands/Server/ServerSchedulerCommand.cs +++ b/source/Cute/Commands/Server/ServerSchedulerCommand.cs @@ -121,7 +121,7 @@ public override async Task ExecuteCommandAsync(CommandContext context, Sett public override void ConfigureWebApplication(WebApplication webApp) { - webApp.MapPost("/reload", RefreshSchedule).DisableAntiforgery(); + webApp.MapPost("/run", RunCommand).DisableAntiforgery(); } public override async Task RenderHomePageBody(HttpContext context) @@ -143,35 +143,85 @@ public override async Task RenderHomePageBody(HttpContext context) foreach (var (key, cronEntry) in _scheduledEntries.Where(k => nextRuns.ContainsKey(k.Key)).OrderBy(kv => nextRuns[kv.Key])) { var entry = cronEntry; + var runningEntry = GetRelatedRunningEntry(entry); while (entry is not null) { - await RenderHomePageTableLines(context, entry, nextRuns[key]); + await RenderHomePageTableLines(context, entry, cronEntry, runningEntry, nextRuns[key]); entry = entry.RunNext; } } await context.Response.WriteAsync($""); - await context.Response.WriteAsync($"
"); + await context.Response.WriteAsync($""); await context.Response.WriteAsync($""); await context.Response.WriteAsync($""); await context.Response.WriteAsync($"
"); + + await context.Response.WriteAsync($"
"); + await context.Response.WriteAsync($""); + await context.Response.WriteAsync($""); + await context.Response.WriteAsync($"
"); + await context.Response.WriteAsync($""); } - private static async Task RenderHomePageTableLines(HttpContext context, ScheduledEntry entry, DateTime nextRunTime) + private async Task RenderHomePageTableLines(HttpContext context, ScheduledEntry entry, ScheduledEntry parentEntry, ScheduledEntry? runningEntry, DateTime nextRunTime) { string? lastRunStarted = entry.LastRunStarted?.ToString("R"); string? lastRunFinished = entry.LastRunFinished?.ToString("R"); string? status = entry.LastRunStatus; string? nextRun = nextRunTime.ToString("R"); + string toRunningDiv = runningEntry == null ? string.Empty : $"
to running
"; + string runningStyle = entry.LastRunStatus == ScheduledEntry.RUNNING ? "style='font-weight:bold;color: green'" : string.Empty; + string? cronExpression = entry.IsRunAfter ? null : entry.Schedule?.ToCronExpression().ToString(); var schedule = entry.IsRunAfter ? "Run after " + entry.RunAfter!.Key : entry.Schedule; await context.Response.WriteAsync($""); - await context.Response.WriteAsync($"{entry.Key}"); + await context.Response.WriteAsync($"{entry.Key}"); + await context.Response.WriteAsync($"
"); + await context.Response.WriteAsync($""); + await context.Response.WriteAsync($""); + await context.Response.WriteAsync($"
"); + await context.Response.WriteAsync($""); await context.Response.WriteAsync($"{schedule}"); - await context.Response.WriteAsync($"{cronExpression}"); + await context.Response.WriteAsync($"
{cronExpression}
"); await context.Response.WriteAsync($""); if (lastRunStarted is not null) @@ -369,17 +419,106 @@ private static void EnsureNewScheduler(ILogger cronLogger) } } - private void RefreshSchedule([FromForm] string command, HttpContext context) + private void RunCommand([FromForm] string command, [FromForm] string? param, HttpContext context) { - if (!command.Equals("reload")) return; + ScheduledEntry? entry = null; + switch (command) + { + case "run_single": + entry = GetScheduleByKey(param!); + if (entry != null && entry.LastRunStatus != ScheduledEntry.RUNNING) + { + _ = Task.Run(() => ProcessContentSyncApi(entry, true)); + } + break; + case "run_chain": + entry = GetScheduleByKey(param!); + if (entry != null && GetRelatedRunningEntry(entry!) == null) + { + _ = Task.Run(() => ProcessContentSyncApi(entry)); + } + break; + case "reload": + EnsureNewScheduler(_cronLogger); + UpdateScheduler(); + _scheduler.Start(); + break; + case "resume_chain": + ResumeBrokenChains(); + break; + default: + break; + } - EnsureNewScheduler(_cronLogger); + context.Response.Redirect($"{_baseUrl}/"); + } - UpdateScheduler(); + private void ResumeBrokenChains() + { + var nextRuns = _scheduler.GetNextOccurrences() + .SelectMany(i => i.ScheduledTasks, (i, j) => new { j.Id, i.NextOccurrence }) + .ToDictionary(o => o.Id, o => o.NextOccurrence); - _scheduler.Start(); + foreach (var (key, cronEntry) in _scheduledEntries.Where(k => nextRuns.ContainsKey(k.Key)).OrderBy(kv => nextRuns[kv.Key])) + { + var entry = cronEntry; - context.Response.Redirect($"{_baseUrl}/"); + while (entry is not null) + { + //await RenderHomePageTableLines(context, entry, nextRuns[key]); + entry = entry.RunNext; + } + } + + foreach (var nextRun in nextRuns) + { + var entry = _scheduledEntries[nextRun.Key]; + var baseStartDate = entry.LastRunStarted; + + while(entry != null && entry.LastRunStarted >= baseStartDate) + { + if (entry.LastRunStatus == CuteSchedule.RUNNING) + { + entry = null; + break; + } + entry = entry.RunNext; + } + + if(entry != null) + { + _ = Task.Run(() => ProcessContentSyncApi(entry)); + } + } + } + + private ScheduledEntry? GetRelatedRunningEntry(ScheduledEntry entry) + { + string parentKey; + if (entry.RunAfter == null) + { + parentKey = entry.Key; + } + else + { + var parentSchedule = entry.RunAfter; + while (parentSchedule.RunAfter != null) + { + parentSchedule = parentSchedule.RunAfter; + } + parentKey = parentSchedule.Key; + } + + var scheduledEntry = _scheduledEntries.Where(k => k.Value.Key == parentKey).FirstOrDefault().Value; + + if (scheduledEntry.LastRunStatus == CuteSchedule.RUNNING) return scheduledEntry; + while(scheduledEntry.RunNext != null) + { + scheduledEntry = scheduledEntry.RunNext; + if (scheduledEntry.LastRunStatus == CuteSchedule.RUNNING) return scheduledEntry; + } + + return null; } private async Task ProcessAndUpdateSchedule(ScheduledEntry entry) @@ -402,11 +541,11 @@ private void DisplaySchedule() } } - private async Task ProcessContentSyncApi(ScheduledEntry CuteSchedule) + private async Task ProcessContentSyncApi(ScheduledEntry cuteSchedule, bool singleRun = false) { string verbosity = _settings?.Verbosity.ToString() ?? Verbosity.Normal.ToString(); - var entry = CuteSchedule; + var entry = cuteSchedule; while (entry is not null) { @@ -414,7 +553,7 @@ private async Task ProcessContentSyncApi(ScheduledEntry CuteSchedule) entry.LastRunStarted = DateTime.UtcNow; entry.LastRunFinished = null; - entry.LastRunStatus = "running"; + entry.LastRunStatus = CuteSchedule.RUNNING; entry.LastRunErrorMessage = string.Empty; try @@ -445,13 +584,13 @@ private async Task ProcessContentSyncApi(ScheduledEntry CuteSchedule) await command.RunAsync(args); - entry.LastRunStatus = "success"; + entry.LastRunStatus = CuteSchedule.SUCCESS; entry.LastRunFinished = DateTime.UtcNow; } catch (Exception ex) { _console.WriteException(ex); - entry.LastRunStatus = $"error"; + entry.LastRunStatus = CuteSchedule.RUNNING; entry.LastRunErrorMessage = $"Exception: {ex.Message} \nTrace: {ex.StackTrace}"; entry.LastRunFinished = DateTime.UtcNow; } @@ -461,6 +600,11 @@ private async Task ProcessContentSyncApi(ScheduledEntry CuteSchedule) await UpdateScheduleEntry(entry); } + if (singleRun) + { + break; + } + entry = entry.RunNext; } } From a3f0930c20a0014ee0441cfb329d599413ede624 Mon Sep 17 00:00:00 2001 From: tengo-lebanidze Date: Mon, 26 Jan 2026 13:08:20 +0400 Subject: [PATCH 2/2] Make action buttons more informative --- source/Cute/Commands/Server/ServerSchedulerCommand.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/source/Cute/Commands/Server/ServerSchedulerCommand.cs b/source/Cute/Commands/Server/ServerSchedulerCommand.cs index 0da2f00..95e625e 100644 --- a/source/Cute/Commands/Server/ServerSchedulerCommand.cs +++ b/source/Cute/Commands/Server/ServerSchedulerCommand.cs @@ -187,12 +187,13 @@ private async Task RenderHomePageTableLines(HttpContext context, ScheduledEntry string toRunningDiv = runningEntry == null ? string.Empty : $"
to running
"; string runningStyle = entry.LastRunStatus == ScheduledEntry.RUNNING ? "style='font-weight:bold;color: green'" : string.Empty; + string parentStyle = entry.Key == parentEntry.Key ? "style='font-weight:bold; font-size:22px'" : string.Empty; string? cronExpression = entry.IsRunAfter ? null : entry.Schedule?.ToCronExpression().ToString(); var schedule = entry.IsRunAfter ? "Run after " + entry.RunAfter!.Key : entry.Schedule; await context.Response.WriteAsync($""); - await context.Response.WriteAsync($"{entry.Key}"); + await context.Response.WriteAsync($"{entry.Key}"); await context.Response.WriteAsync($"
"); await context.Response.WriteAsync($""); await context.Response.WriteAsync($""); await context.Response.WriteAsync($"
");