Task groups & viewer in admin panel #52 (#91)

* feat: historical tasks in database, better scheduling, and unified API for accessing tasks

* feat: new UI for everything

* fix: add translations and fix formatting
This commit is contained in:
DecDuck
2025-06-07 15:39:01 +10:00
committed by GitHub
parent d976ac87e3
commit 4184705b14
15 changed files with 468 additions and 105 deletions

View File

@ -1,5 +1,6 @@
import droplet from "@drop-oss/droplet";
import type { MinimumRequestObject } from "~/server/h3";
import type { GlobalACL } from "../acls";
import aclManager from "../acls";
import cleanupInvites from "./registry/invitations";
@ -7,6 +8,7 @@ import cleanupSessions from "./registry/sessions";
import checkUpdate from "./registry/update";
import cleanupObjects from "./registry/objects";
import { taskGroups, type TaskGroup } from "./group";
import prisma from "../db/database";
// a task that has been run
type FinishedTask = {
@ -36,15 +38,19 @@ type TaskPoolEntry = FinishedTask & {
*/
class TaskHandler {
// registry of schedualed tasks to be created
private scheduledTasks: Map<TaskGroup, () => Task> = new Map();
// list of all finished tasks
private finishedTasks: Map<string, FinishedTask> = new Map();
private taskCreators: Map<TaskGroup, () => Task> = new Map();
// list of all currently running tasks
private taskPool = new Map<string, TaskPoolEntry>();
// list of all clients currently connected to tasks
private clientRegistry = new Map<string, PeerImpl>();
private scheduledTasks: TaskGroup[] = [
"cleanup:invitations",
"cleanup:sessions",
"check:update",
];
constructor() {
// register the cleanup invitations task
this.saveScheduledTask(cleanupInvites);
@ -58,7 +64,7 @@ class TaskHandler {
* @param createTask
*/
private saveScheduledTask(task: DropTask) {
this.scheduledTasks.set(task.taskGroup, task.build);
this.taskCreators.set(task.taskGroup, task.build);
}
create(task: Task) {
@ -129,7 +135,7 @@ class TaskHandler {
const taskEntry = this.taskPool.get(task.id);
if (!taskEntry) return;
taskEntry.log.push(entry);
console.log(`[Task ${task.taskGroup}]: ${entry}`);
// console.log(`[Task ${task.taskGroup}]: ${entry}`);
updateAllClients();
};
@ -171,10 +177,25 @@ class TaskHandler {
this.disconnect(clientId, task.id);
}
// so we can drop the clients from the task entry
const { clients, ...copied } = taskEntry;
this.finishedTasks.set(task.id, {
...copied,
await prisma.task.create({
data: {
id: task.id,
taskGroup: taskEntry.taskGroup,
name: taskEntry.name,
started: taskEntry.startTime,
ended: taskEntry.endTime,
success: taskEntry.success,
progress: taskEntry.progress,
log: taskEntry.log,
acls: taskEntry.acls,
...(taskEntry.error
? { error: JSON.stringify(taskEntry.error) }
: undefined),
},
});
this.taskPool.delete(task.id);
@ -187,7 +208,9 @@ class TaskHandler {
peer: PeerImpl,
request: MinimumRequestObject,
) {
const task = this.taskPool.get(taskId);
const task =
this.taskPool.get(taskId) ??
(await prisma.task.findUnique({ where: { id: taskId } }));
if (!task) {
peer.send(
`error/${taskId}/Unknown task/Drop couldn't find the task you're looking for.`,
@ -205,13 +228,17 @@ class TaskHandler {
}
this.clientRegistry.set(clientId, peer);
task.clients.set(clientId, true); // Uniquely insert client to avoid sending duplicate traffic
if ("clients" in task) {
task.clients.set(clientId, true); // Uniquely insert client to avoid sending duplicate traffic
}
const catchupMessage: TaskMessage = {
id: taskId,
name: task.name,
success: task.success,
error: task.error,
error: task.error as unknown as
| { title: string; description: string }
| undefined,
log: task.log,
progress: task.progress,
};
@ -253,8 +280,19 @@ class TaskHandler {
return true;
}
runningTasks() {
return this.taskPool
.entries()
.map(([id, value]) => ({ ...value, id, log: undefined }))
.toArray();
}
dailyTasks() {
return this.scheduledTasks;
}
runTaskGroupByName(name: TaskGroup) {
const task = this.scheduledTasks.get(name);
const task = this.taskCreators.get(name);
if (!task) {
console.warn(`No task found for group ${name}`);
return;
@ -262,13 +300,30 @@ class TaskHandler {
this.create(task());
}
/**]
/**
* Runs all daily tasks that are scheduled to run once a day.
*/
triggerDailyTasks() {
this.runTaskGroupByName("cleanup:invitations");
this.runTaskGroupByName("cleanup:sessions");
this.runTaskGroupByName("check:update");
async triggerDailyTasks() {
for (const taskGroup of this.scheduledTasks) {
const mostRecent = await prisma.task.findFirst({
where: {
taskGroup,
},
orderBy: {
ended: "desc",
},
});
if (mostRecent) {
const currentTime = Date.now();
const lastRun = mostRecent.ended.getTime();
const difference = currentTime - lastRun;
if (difference < 1000 * 60 * 60 * 24) {
// If it's been less than one day
continue; // skip
}
}
await this.runTaskGroupByName(taskGroup);
}
}
}
@ -282,7 +337,7 @@ export interface Task {
taskGroup: TaskGroup;
name: string;
run: (context: TaskRunContext) => Promise<void>;
acls: string[];
acls: GlobalACL[];
}
export type TaskMessage = {
@ -304,7 +359,7 @@ export interface BuildTask {
taskGroup: TaskGroup;
name: string;
run: (context: TaskRunContext) => Promise<void>;
acls: string[];
acls: GlobalACL[];
}
interface DropTask {

View File

@ -4,7 +4,7 @@ import { defineDropTask } from "..";
export default defineDropTask({
buildId: () => `cleanup:invitations:${new Date().toISOString()}`,
name: "Cleanup Invitations",
acls: [],
acls: ["system:maintenance:read"],
taskGroup: "cleanup:invitations",
async run({ log }) {
log("Cleaning invitations");

View File

@ -13,7 +13,7 @@ type FieldReferenceMap = {
export default defineDropTask({
buildId: () => `cleanup:objects:${new Date().toISOString()}`,
name: "Cleanup Objects",
acls: [],
acls: ["system:maintenance:read"],
taskGroup: "cleanup:objects",
async run({ progress, log }) {
log("Cleaning unreferenced objects");

View File

@ -4,7 +4,7 @@ import { defineDropTask } from "..";
export default defineDropTask({
buildId: () => `cleanup:sessions:${new Date().toISOString()}`,
name: "Cleanup Sessions",
acls: [],
acls: ["system:maintenance:read"],
taskGroup: "cleanup:sessions",
async run({ log }) {
log("Cleaning up sessions");

View File

@ -19,7 +19,7 @@ const latestRelease = type({
export default defineDropTask({
buildId: () => `check:update:${new Date().toISOString()}`,
name: "Check for Update",
acls: [],
acls: ["system:maintenance:read"],
taskGroup: "check:update",
async run({ log }) {
// TODO: maybe implement some sort of rate limit thing to prevent this from calling github api a bunch in the event of crashloop or whatever?