[backend/razor] Add cron tasks page to admin dashboard (ISH-719)
This commit is contained in:
parent
94c07d3d06
commit
10b74ff0d9
6 changed files with 97 additions and 6 deletions
|
@ -14,6 +14,7 @@
|
||||||
new("/admin/users", "User management", Icons.Users),
|
new("/admin/users", "User management", Icons.Users),
|
||||||
new("/admin/federation", "Federation control", Icons.Graph),
|
new("/admin/federation", "Federation control", Icons.Graph),
|
||||||
new("/admin/relays", "Relays", Icons.FastForward),
|
new("/admin/relays", "Relays", Icons.FastForward),
|
||||||
|
new("/admin/tasks", "Cron tasks", Icons.Timer),
|
||||||
new("/admin/plugins", "Plugins", Icons.Plug)
|
new("/admin/plugins", "Plugins", Icons.Plug)
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -298,6 +298,17 @@ public class AdminController(
|
||||||
await new MediaCleanupTask().InvokeAsync(scope.ServiceProvider);
|
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")]
|
[HttpGet("policy")]
|
||||||
[ProducesResults(HttpStatusCode.OK)]
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
public async Task<List<string>> GetAvailablePolicies() => await policySvc.GetAvailablePoliciesAsync();
|
public async Task<List<string>> GetAvailablePolicies() => await policySvc.GetAvailablePoliciesAsync();
|
||||||
|
|
|
@ -2,5 +2,33 @@ namespace Iceshrimp.Backend.Core.Extensions;
|
||||||
|
|
||||||
public static class TimeSpanExtensions
|
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);
|
public static long GetTotalMilliseconds(this TimeSpan timeSpan) => Convert.ToInt64(timeSpan.TotalMilliseconds);
|
||||||
}
|
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,16 +6,27 @@ namespace Iceshrimp.Backend.Core.Services;
|
||||||
|
|
||||||
public class CronService(IServiceScopeFactory serviceScopeFactory) : BackgroundService
|
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)
|
protected override Task ExecuteAsync(CancellationToken token)
|
||||||
{
|
{
|
||||||
var tasks = PluginLoader
|
Tasks = PluginLoader
|
||||||
.Assemblies.Prepend(Assembly.GetExecutingAssembly())
|
.Assemblies.Prepend(Assembly.GetExecutingAssembly())
|
||||||
.SelectMany(AssemblyLoader.GetImplementationsOfInterface<ICronTask>)
|
.SelectMany(AssemblyLoader.GetImplementationsOfInterface<ICronTask>)
|
||||||
|
.OrderBy(p => p.AssemblyQualifiedName)
|
||||||
|
.ThenBy(p => p.Name)
|
||||||
.Select(p => Activator.CreateInstance(p) as ICronTask)
|
.Select(p => Activator.CreateInstance(p) as ICronTask)
|
||||||
.Where(p => p != null)
|
.Where(p => p != null)
|
||||||
.Cast<ICronTask>();
|
.Cast<ICronTask>()
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
foreach (var task in tasks)
|
foreach (var task in Tasks)
|
||||||
{
|
{
|
||||||
ICronTrigger trigger = task.Type switch
|
ICronTrigger trigger = task.Type switch
|
||||||
{
|
{
|
||||||
|
@ -28,8 +39,7 @@ public class CronService(IServiceScopeFactory serviceScopeFactory) : BackgroundS
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await using var scope = serviceScopeFactory.CreateAsyncScope();
|
await RunCronTaskAsync(task);
|
||||||
await task.InvokeAsync(scope.ServiceProvider);
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|
37
Iceshrimp.Backend/Pages/Admin/Tasks.razor
Normal file
37
Iceshrimp.Backend/Pages/Admin/Tasks.razor
Normal file
|
@ -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
|
||||||
|
<AdminPageHeader Title="Cron tasks"/>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Assembly</th>
|
||||||
|
<th>Schedule</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@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()
|
||||||
|
};
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>@type.Name</td>
|
||||||
|
<td>@type.Assembly.GetName().Name</td>
|
||||||
|
<td>@schedule</td>
|
||||||
|
<td><a class="fake-link" onclick="runCronTask('@type.FullName', event.target)">Run now</a></td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
|
@ -54,6 +54,10 @@ async function purgeUser(id, target) {
|
||||||
await confirm(target, () => callApiMethod(`/api/iceshrimp/moderation/users/${id}/purge`));
|
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() {
|
async function generateInvite() {
|
||||||
const res = await callApiMethod(`/api/iceshrimp/admin/invites/generate`);
|
const res = await callApiMethod(`/api/iceshrimp/admin/invites/generate`);
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
|
|
Loading…
Add table
Reference in a new issue