From 10b74ff0d9e2680089035e6ee0c629c7ecbff9a6 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Sat, 15 Mar 2025 03:55:09 +0100 Subject: [PATCH] [backend/razor] Add cron tasks page to admin dashboard (ISH-719) --- .../Components/Admin/AdminNav.razor | 1 + .../Controllers/Web/AdminController.cs | 11 ++++++ .../Core/Extensions/TimeSpanExtensions.cs | 30 ++++++++++++++- .../Core/Services/CronService.cs | 20 +++++++--- Iceshrimp.Backend/Pages/Admin/Tasks.razor | 37 +++++++++++++++++++ Iceshrimp.Backend/wwwroot/js/admin.js | 4 ++ 6 files changed, 97 insertions(+), 6 deletions(-) create mode 100644 Iceshrimp.Backend/Pages/Admin/Tasks.razor diff --git a/Iceshrimp.Backend/Components/Admin/AdminNav.razor b/Iceshrimp.Backend/Components/Admin/AdminNav.razor index 9fdcde1d..32518d78 100644 --- a/Iceshrimp.Backend/Components/Admin/AdminNav.razor +++ b/Iceshrimp.Backend/Components/Admin/AdminNav.razor @@ -14,6 +14,7 @@ new("/admin/users", "User management", Icons.Users), new("/admin/federation", "Federation control", Icons.Graph), new("/admin/relays", "Relays", Icons.FastForward), + new("/admin/tasks", "Cron tasks", Icons.Timer), new("/admin/plugins", "Plugins", Icons.Plug) ]; diff --git a/Iceshrimp.Backend/Controllers/Web/AdminController.cs b/Iceshrimp.Backend/Controllers/Web/AdminController.cs index b0498a8e..b2fa386a 100644 --- a/Iceshrimp.Backend/Controllers/Web/AdminController.cs +++ b/Iceshrimp.Backend/Controllers/Web/AdminController.cs @@ -298,6 +298,17 @@ public class AdminController( await new MediaCleanupTask().InvokeAsync(scope.ServiceProvider); } + [HttpPost("tasks/{id}/run")] + [ProducesResults(HttpStatusCode.OK)] + [ProducesErrors(HttpStatusCode.NotFound)] + public void RunCronTask([FromServices] CronService cronSvc, string id) + { + var task = cronSvc.Tasks.FirstOrDefault(p => p.GetType().FullName == id) + ?? throw GracefulException.NotFound("Task not found"); + + _ = cronSvc.RunCronTaskAsync(task); + } + [HttpGet("policy")] [ProducesResults(HttpStatusCode.OK)] public async Task> GetAvailablePolicies() => await policySvc.GetAvailablePoliciesAsync(); diff --git a/Iceshrimp.Backend/Core/Extensions/TimeSpanExtensions.cs b/Iceshrimp.Backend/Core/Extensions/TimeSpanExtensions.cs index 7498f655..75becade 100644 --- a/Iceshrimp.Backend/Core/Extensions/TimeSpanExtensions.cs +++ b/Iceshrimp.Backend/Core/Extensions/TimeSpanExtensions.cs @@ -2,5 +2,33 @@ namespace Iceshrimp.Backend.Core.Extensions; public static class TimeSpanExtensions { + private static readonly long Seconds = TimeSpan.FromMinutes(1).Ticks; + private static readonly long Minutes = TimeSpan.FromHours(1).Ticks; + private static readonly long Hours = TimeSpan.FromDays(1).Ticks; + public static long GetTotalMilliseconds(this TimeSpan timeSpan) => Convert.ToInt64(timeSpan.TotalMilliseconds); -} \ No newline at end of file + + public static string ToDisplayString(this TimeSpan timeSpan, bool singleNumber = true) + { + if (timeSpan.Ticks < Seconds) + { + var seconds = (int)timeSpan.TotalSeconds; + return seconds == 1 ? singleNumber ? "1 second" : "second" : $"{seconds} seconds"; + } + + if (timeSpan.Ticks < Minutes) + { + var minutes = (int)timeSpan.TotalMinutes; + return minutes == 1 ? singleNumber ? "1 minute" : "minute" : $"{timeSpan.TotalMinutes} minutes"; + } + + if (timeSpan.Ticks < Hours) + { + var hours = (int)timeSpan.TotalHours; + return hours == 1 ? singleNumber ? "1 hour" : "hour" : $"{hours} hours"; + } + + var days = (int)timeSpan.TotalDays; + return days == 1 ? singleNumber ? "1 day" : "day" : $"{timeSpan.TotalDays} days"; + } +} diff --git a/Iceshrimp.Backend/Core/Services/CronService.cs b/Iceshrimp.Backend/Core/Services/CronService.cs index 480f64e0..b78757bf 100644 --- a/Iceshrimp.Backend/Core/Services/CronService.cs +++ b/Iceshrimp.Backend/Core/Services/CronService.cs @@ -6,16 +6,27 @@ namespace Iceshrimp.Backend.Core.Services; public class CronService(IServiceScopeFactory serviceScopeFactory) : BackgroundService { + public ICronTask[] Tasks { get; private set; } = []; + + public async Task RunCronTaskAsync(ICronTask task) + { + await using var scope = serviceScopeFactory.CreateAsyncScope(); + await task.InvokeAsync(scope.ServiceProvider); + } + protected override Task ExecuteAsync(CancellationToken token) { - var tasks = PluginLoader + Tasks = PluginLoader .Assemblies.Prepend(Assembly.GetExecutingAssembly()) .SelectMany(AssemblyLoader.GetImplementationsOfInterface) + .OrderBy(p => p.AssemblyQualifiedName) + .ThenBy(p => p.Name) .Select(p => Activator.CreateInstance(p) as ICronTask) .Where(p => p != null) - .Cast(); + .Cast() + .ToArray(); - foreach (var task in tasks) + foreach (var task in Tasks) { ICronTrigger trigger = task.Type switch { @@ -28,8 +39,7 @@ public class CronService(IServiceScopeFactory serviceScopeFactory) : BackgroundS { try { - await using var scope = serviceScopeFactory.CreateAsyncScope(); - await task.InvokeAsync(scope.ServiceProvider); + await RunCronTaskAsync(task); } catch { diff --git a/Iceshrimp.Backend/Pages/Admin/Tasks.razor b/Iceshrimp.Backend/Pages/Admin/Tasks.razor new file mode 100644 index 00000000..694991fa --- /dev/null +++ b/Iceshrimp.Backend/Pages/Admin/Tasks.razor @@ -0,0 +1,37 @@ +@page "/admin/tasks" +@using Iceshrimp.Backend.Components.Admin +@using Iceshrimp.Backend.Core.Extensions +@using Iceshrimp.Backend.Core.Services +@inherits AdminComponentBase +@inject CronService CronSvc + + + + + + + + + + + @foreach (var task in CronSvc.Tasks) + { + var type = task.GetType(); + var schedule = task.Type switch + { + CronTaskType.Daily when task.Trigger == TimeSpan.Zero => "daily at midnight", + // + CronTaskType.Daily => $"daily at {((int)task.Trigger.TotalHours).ToString().PadLeft(2, '0')}:{((int)task.Trigger.TotalMinutes).ToString().PadLeft(2, '0')}", + CronTaskType.Interval => $"every {task.Trigger.ToDisplayString(singleNumber: false)}", + _ => throw new ArgumentOutOfRangeException() + }; + + + + + + + + } + +
NameAssemblyScheduleActions
@type.Name@type.Assembly.GetName().Name@scheduleRun now
\ No newline at end of file diff --git a/Iceshrimp.Backend/wwwroot/js/admin.js b/Iceshrimp.Backend/wwwroot/js/admin.js index ed9cd9b7..ed300f7c 100644 --- a/Iceshrimp.Backend/wwwroot/js/admin.js +++ b/Iceshrimp.Backend/wwwroot/js/admin.js @@ -54,6 +54,10 @@ async function purgeUser(id, target) { await confirm(target, () => callApiMethod(`/api/iceshrimp/moderation/users/${id}/purge`)); } +async function runCronTask(id, target) { + await confirm(target, () => callApiMethod(`/api/iceshrimp/admin/tasks/${id}/run`)); +} + async function generateInvite() { const res = await callApiMethod(`/api/iceshrimp/admin/invites/generate`); const json = await res.json();