Files
ams-backend/src/routes/tasks.ts
FluxKit bebfcbb816 feat: Add Gitea integration
- New /api/gitea/* routes for Gitea API
- List projects, branches, commits
- File tree and content viewing
- User token management
2026-02-19 14:07:41 +00:00

898 lines
27 KiB
TypeScript

import { Router } from "@oak/oak";
import { ObjectId } from "mongodb";
import { getDB } from "../db/mongo.ts";
import { authMiddleware } from "../middleware/auth.ts";
import { logTaskChanges } from "../utils/taskChangelog.ts";
import { events, AMS_EVENTS } from "../utils/eventEmitter.ts";
export const tasksRouter = new Router({ prefix: "/api/tasks" });
// --- Counter Helper für Nummernkreise ---
export async function getNextNumber(counterName: string): Promise<number> {
const db = await getDB();
const result = await db.collection("counters").findOneAndUpdate(
{ _id: counterName },
{ $inc: { seq: 1 } },
{ upsert: true, returnDocument: "after" }
);
return (result as unknown as { seq: number }).seq;
}
interface Reminder {
enabled: boolean;
datetime: Date; // Next trigger time
interval: 'once' | 'daily' | 'hourly' | 'minutes';
intervalValue?: number; // For 'minutes': every X minutes
lastNotified?: Date;
}
interface Task {
_id: ObjectId;
number?: number;
title: string;
description?: string;
status: "backlog" | "todo" | "in_progress" | "review" | "done";
priority: "low" | "medium" | "high" | "urgent";
assignee?: string; // Agent ID
project?: string; // Project ID
labels: string[];
dueDate?: Date;
reminder?: Reminder;
createdBy: string;
createdAt: Date;
updatedAt: Date;
}
interface GiteaProjectRef {
projectId: number;
path: string;
url: string;
name: string;
}
interface GitLabProjectRef {
projectId: number;
path: string;
url: string;
name: string;
}
interface Project {
_id: ObjectId;
name: string;
description?: string;
color: string;
rules?: string;
// GitLab Integration - Multiple projects
gitlabProjects?: GitLabProjectRef[];
// Legacy single project (for backwards compatibility)
gitlabProjectId?: number;
gitlabUrl?: string;
gitlabPath?: string;
// Gitea Integration
giteaProjects?: GiteaProjectRef[];
createdAt: Date;
updatedAt: Date;
}
// ============ TASKS ============
// Get all tasks (with optional filters)
tasksRouter.get("/", authMiddleware, async (ctx) => {
const db = await getDB();
const url = ctx.request.url;
const filter: Record<string, unknown> = {};
const status = url.searchParams.get("status");
if (status) filter.status = status;
const assignee = url.searchParams.get("assignee");
if (assignee) filter.assignee = assignee;
const project = url.searchParams.get("project");
if (project) filter.project = project;
const priority = url.searchParams.get("priority");
if (priority) filter.priority = priority;
const tasks = await db.collection<Task>("tasks")
.find(filter)
.sort({ priority: -1, createdAt: -1 })
.toArray();
ctx.response.body = { tasks };
});
// Get task by number
tasksRouter.get("/by-number/:number", authMiddleware, async (ctx) => {
const db = await getDB();
const num = parseInt(ctx.params.number);
if (isNaN(num)) {
ctx.response.status = 400;
ctx.response.body = { error: "Invalid number" };
return;
}
const task = await db.collection<Task>("tasks").findOne({ number: num });
if (!task) {
ctx.response.status = 404;
ctx.response.body = { error: "Task not found" };
return;
}
ctx.response.body = { task };
});
// Get task by ID
tasksRouter.get("/:id", authMiddleware, async (ctx) => {
const db = await getDB();
const id = ctx.params.id;
try {
const task = await db.collection<Task>("tasks").findOne({ _id: new ObjectId(id) });
if (!task) {
ctx.response.status = 404;
ctx.response.body = { error: "Task not found" };
return;
}
ctx.response.body = { task };
} catch {
ctx.response.status = 400;
ctx.response.body = { error: "Invalid task ID" };
}
});
// Create task
tasksRouter.post("/", authMiddleware, async (ctx) => {
const body = await ctx.request.body.json();
const { title, description, status, priority, assignee, project, labels, dueDate } = body;
if (!title) {
ctx.response.status = 400;
ctx.response.body = { error: "Title is required" };
return;
}
const db = await getDB();
const now = new Date();
const number = await getNextNumber("task_number");
const result = await db.collection<Task>("tasks").insertOne({
number,
title,
description: description || "",
status: status || "backlog",
priority: priority || "medium",
assignee,
project,
labels: labels || [],
dueDate: dueDate ? new Date(dueDate) : undefined,
createdBy: ctx.state.user.id,
createdAt: now,
updatedAt: now
} as Task);
const taskId = result.insertedId.toString();
events.emit(AMS_EVENTS.TASK_CREATED, { taskId, title, status: status || "backlog", priority: priority || "medium" });
ctx.response.status = 201;
ctx.response.body = {
message: "Task created",
id: taskId
};
});
// Update task
tasksRouter.put("/:id", authMiddleware, async (ctx) => {
const id = ctx.params.id;
const body = await ctx.request.body.json();
const { title, description, status, priority, assignee, project, labels, dueDate } = body;
const db = await getDB();
const updateFields: Partial<Task> = { updatedAt: new Date() };
if (title !== undefined) updateFields.title = title;
if (description !== undefined) updateFields.description = description;
if (status !== undefined) updateFields.status = status;
if (priority !== undefined) updateFields.priority = priority;
if (assignee !== undefined) updateFields.assignee = assignee;
if (project !== undefined) updateFields.project = project;
if (labels !== undefined) updateFields.labels = labels;
if (dueDate !== undefined) updateFields.dueDate = dueDate ? new Date(dueDate) : undefined;
try {
// Load old task for changelog
const oldTask = await db.collection<Task>("tasks").findOne({ _id: new ObjectId(id) });
if (!oldTask) {
ctx.response.status = 404;
ctx.response.body = { error: "Task not found" };
return;
}
const result = await db.collection<Task>("tasks").updateOne(
{ _id: new ObjectId(id) },
{ $set: updateFields }
);
if (result.matchedCount === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Task not found" };
return;
}
// Log changes
await logTaskChanges(id, oldTask as unknown as Record<string, unknown>, updateFields, ctx.state.user.id);
events.emit(AMS_EVENTS.TASK_UPDATED, { taskId: id, changes: updateFields });
ctx.response.body = { message: "Task updated" };
} catch {
ctx.response.status = 400;
ctx.response.body = { error: "Invalid task ID" };
}
});
// Update task status only (for drag & drop)
tasksRouter.patch("/:id/status", authMiddleware, async (ctx) => {
const id = ctx.params.id;
const body = await ctx.request.body.json();
const { status } = body;
if (!["backlog", "todo", "in_progress", "review", "done"].includes(status)) {
ctx.response.status = 400;
ctx.response.body = { error: "Invalid status" };
return;
}
const db = await getDB();
try {
const oldTask = await db.collection<Task>("tasks").findOne({ _id: new ObjectId(id) });
if (!oldTask) {
ctx.response.status = 404;
ctx.response.body = { error: "Task not found" };
return;
}
const result = await db.collection<Task>("tasks").updateOne(
{ _id: new ObjectId(id) },
{ $set: { status, updatedAt: new Date() } }
);
if (result.matchedCount === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Task not found" };
return;
}
await logTaskChanges(id, oldTask as unknown as Record<string, unknown>, { status }, ctx.state.user.id);
events.emit(AMS_EVENTS.TASK_STATUS_CHANGED, { taskId: id, oldStatus: oldTask.status, newStatus: status });
ctx.response.body = { message: "Status updated" };
} catch {
ctx.response.status = 400;
ctx.response.body = { error: "Invalid task ID" };
}
});
// Delete task (with cascade delete of comments and attachments)
tasksRouter.delete("/:id", authMiddleware, async (ctx) => {
const id = ctx.params.id;
const db = await getDB();
try {
// First, get all comments for this task to delete their attachments
const comments = await db.collection("comments")
.find({ $or: [{ taskId: id }, { parentType: "task", parentId: id }] })
.toArray();
// Delete attachments for each comment
for (const comment of comments) {
await db.collection("attachments").deleteMany({
parentType: "comment",
parentId: comment._id.toString()
});
}
// Delete all comments for this task (both old taskId format and new parentType format)
await db.collection("comments").deleteMany({
$or: [{ taskId: id }, { parentType: "task", parentId: id }]
});
// Delete all attachments directly on task (both old taskId format and new parentType format)
await db.collection("attachments").deleteMany({
$or: [{ taskId: id }, { parentType: "task", parentId: id }]
});
// Finally delete the task
const result = await db.collection<Task>("tasks").deleteOne({ _id: new ObjectId(id) });
if (result.deletedCount === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Task not found" };
return;
}
events.emit(AMS_EVENTS.TASK_DELETED, { taskId: id });
ctx.response.body = { message: "Task and related data deleted" };
} catch {
ctx.response.status = 400;
ctx.response.body = { error: "Invalid task ID" };
}
});
// Get task changelog
tasksRouter.get("/:id/changelog", authMiddleware, async (ctx) => {
const taskId = ctx.params.id;
const db = await getDB();
try {
// Validate task exists
const task = await db.collection("tasks").findOne({ _id: new ObjectId(taskId) });
if (!task) {
ctx.response.status = 404;
ctx.response.body = { error: "Task not found" };
return;
}
const entries = await db.collection("task_changelog")
.find({ taskId })
.sort({ changedAt: -1 })
.limit(100)
.toArray();
// Resolve changedBy IDs to names
const userIds = [...new Set(entries.map(e => e.changedBy).filter(id => id && !id.startsWith("service:")))];
const nameMap: Record<string, string> = {};
// Service accounts
for (const entry of entries) {
if (entry.changedBy?.startsWith("service:")) {
const agentName = entry.changedBy.replace("service:", "");
nameMap[entry.changedBy] = agentName.charAt(0).toUpperCase() + agentName.slice(1);
}
}
// Lookup users
if (userIds.length > 0) {
const objectIds = userIds.reduce<ObjectId[]>((acc, id) => {
try { acc.push(new ObjectId(id)); } catch { /* skip invalid */ }
return acc;
}, []);
if (objectIds.length > 0) {
const users = await db.collection("users").find({ _id: { $in: objectIds } }).toArray();
for (const user of users) {
nameMap[user._id.toString()] = user.name || user.email || user._id.toString();
}
// Also check agents collection
const agents = await db.collection("agents").find({ _id: { $in: objectIds } }).toArray();
for (const agent of agents) {
nameMap[agent._id.toString()] = agent.emoji ? `${agent.emoji} ${agent.name}` : agent.name;
}
}
}
const enrichedEntries = entries.map(e => ({
...e,
changedByName: nameMap[e.changedBy] || e.changedBy,
}));
ctx.response.body = { changelog: enrichedEntries };
} catch {
ctx.response.status = 400;
ctx.response.body = { error: "Invalid task ID" };
}
});
// ============ PROJECTS ============
// Get all projects
tasksRouter.get("/projects/list", authMiddleware, async (ctx) => {
const db = await getDB();
const projects = await db.collection<Project>("projects")
.find({})
.sort({ name: 1 })
.toArray();
ctx.response.body = { projects };
});
// Create project
tasksRouter.post("/projects", authMiddleware, async (ctx) => {
if (ctx.state.user.role !== "admin") {
ctx.response.status = 403;
ctx.response.body = { error: "Admin access required" };
return;
}
const body = await ctx.request.body.json();
const { name, description, color, gitlabProjectId, gitlabUrl, gitlabPath, gitlabProjects, giteaProjects, rules } = body;
if (!name) {
ctx.response.status = 400;
ctx.response.body = { error: "Name is required" };
return;
}
const db = await getDB();
const now = new Date();
const result = await db.collection<Project>("projects").insertOne({
name,
description: description || "",
color: color || "#6366f1",
gitlabProjectId: gitlabProjectId || undefined,
gitlabUrl: gitlabUrl || undefined,
gitlabPath: gitlabPath || undefined,
gitlabProjects: gitlabProjects || [],
giteaProjects: giteaProjects || [],
rules: rules || "",
createdAt: now,
updatedAt: now
} as Project);
ctx.response.status = 201;
ctx.response.body = {
message: "Project created",
id: result.insertedId.toString()
};
});
// Update project
tasksRouter.put("/projects/:id", authMiddleware, async (ctx) => {
if (ctx.state.user.role !== "admin") {
ctx.response.status = 403;
ctx.response.body = { error: "Admin access required" };
return;
}
const id = ctx.params.id;
const body = await ctx.request.body.json();
const { name, description, color, rules, gitlabProjectId, gitlabUrl, gitlabPath, gitlabProjects, giteaProjects } = body;
const db = await getDB();
const updateFields: Partial<Project> = { updatedAt: new Date() };
if (name !== undefined) updateFields.name = name;
if (description !== undefined) updateFields.description = description;
if (color !== undefined) updateFields.color = color;
if (rules !== undefined) (updateFields as any).rules = rules;
// GitLab fields - Multiple projects (new)
if (gitlabProjects !== undefined) updateFields.gitlabProjects = gitlabProjects || [];
if (giteaProjects !== undefined) (updateFields as any).giteaProjects = giteaProjects || [];
// Legacy single project fields (for backwards compatibility)
if (gitlabProjectId !== undefined) updateFields.gitlabProjectId = gitlabProjectId || undefined;
if (gitlabUrl !== undefined) updateFields.gitlabUrl = gitlabUrl || undefined;
if (gitlabPath !== undefined) updateFields.gitlabPath = gitlabPath || undefined;
try {
const result = await db.collection<Project>("projects").updateOne(
{ _id: new ObjectId(id) },
{ $set: updateFields }
);
if (result.matchedCount === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Project not found" };
return;
}
ctx.response.body = { message: "Project updated" };
} catch {
ctx.response.status = 400;
ctx.response.body = { error: "Invalid project ID" };
}
});
// Delete project
tasksRouter.delete("/projects/:id", authMiddleware, async (ctx) => {
if (ctx.state.user.role !== "admin") {
ctx.response.status = 403;
ctx.response.body = { error: "Admin access required" };
return;
}
const id = ctx.params.id;
const db = await getDB();
try {
const result = await db.collection<Project>("projects").deleteOne({ _id: new ObjectId(id) });
if (result.deletedCount === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Project not found" };
return;
}
ctx.response.body = { message: "Project deleted" };
} catch {
ctx.response.status = 400;
ctx.response.body = { error: "Invalid project ID" };
}
});
// ============ GITLAB COMMITS ============
interface TaskCommit {
_id: ObjectId;
taskId: string;
commitSha: string;
shortId: string;
gitlabProjectId: number;
gitlabProjectPath: string;
commitTitle: string;
commitAuthor: string;
commitDate: string;
commitUrl: string;
linkedBy: string;
linkedByName: string;
linkedAt: Date;
}
// GET /api/tasks/:id/commits - Verknüpfte Commits abrufen
tasksRouter.get("/:id/commits", authMiddleware, async (ctx) => {
const taskId = ctx.params.id;
const db = await getDB();
try {
const task = await db.collection("tasks").findOne({ _id: new ObjectId(taskId) });
if (!task) {
ctx.response.status = 404;
ctx.response.body = { error: "Task not found" };
return;
}
const commits = await db.collection<TaskCommit>("task_commits")
.find({ taskId })
.sort({ linkedAt: -1 })
.toArray();
ctx.response.body = { commits };
} catch {
ctx.response.status = 400;
ctx.response.body = { error: "Invalid task ID" };
}
});
// POST /api/tasks/:id/commits - Commit mit Task verknüpfen
tasksRouter.post("/:id/commits", authMiddleware, async (ctx) => {
const taskId = ctx.params.id;
const body = await ctx.request.body.json();
const { commitSha, shortId, gitlabProjectId, gitlabProjectPath, commitTitle, commitAuthor, commitDate, commitUrl } = body;
if (!commitSha || !gitlabProjectId) {
ctx.response.status = 400;
ctx.response.body = { error: "commitSha and gitlabProjectId are required" };
return;
}
const db = await getDB();
try {
const task = await db.collection("tasks").findOne({ _id: new ObjectId(taskId) });
if (!task) {
ctx.response.status = 404;
ctx.response.body = { error: "Task not found" };
return;
}
// Prüfen ob schon verknüpft
const existing = await db.collection<TaskCommit>("task_commits").findOne({
taskId,
commitSha,
gitlabProjectId,
});
if (existing) {
ctx.response.status = 409;
ctx.response.body = { error: "Commit already linked to this task" };
return;
}
const now = new Date();
const result = await db.collection<TaskCommit>("task_commits").insertOne({
taskId,
commitSha,
shortId: shortId || commitSha.substring(0, 8),
gitlabProjectId,
gitlabProjectPath: gitlabProjectPath || "",
commitTitle: commitTitle || "",
commitAuthor: commitAuthor || "",
commitDate: commitDate || now.toISOString(),
commitUrl: commitUrl || "",
linkedBy: ctx.state.user.id,
linkedByName: ctx.state.user.username,
linkedAt: now,
} as TaskCommit);
// Changelog-Eintrag
await db.collection("task_changelog").insertOne({
taskId,
field: "commits",
oldValue: null,
newValue: `${shortId || commitSha.substring(0, 8)} - ${commitTitle}`,
changedBy: ctx.state.user.id,
changedAt: now,
});
// Log-Eintrag
await db.collection("logs").insertOne({
agentId: ctx.state.user.id,
agentName: ctx.state.user.username,
level: "info",
message: `Commit ${shortId || commitSha.substring(0, 8)} mit Task "${task.title}" verknüpft`,
metadata: { taskId, commitSha, gitlabProjectId, gitlabProjectPath },
timestamp: now,
});
events.emit(AMS_EVENTS.TASK_UPDATED, { taskId, changes: { commits: "added" } });
ctx.response.status = 201;
ctx.response.body = { message: "Commit linked", id: result.insertedId.toString() };
} catch {
ctx.response.status = 400;
ctx.response.body = { error: "Invalid task ID" };
}
});
// DELETE /api/tasks/:id/commits/:commitId - Commit-Verknüpfung entfernen
tasksRouter.delete("/:id/commits/:commitId", authMiddleware, async (ctx) => {
const taskId = ctx.params.id;
const commitId = ctx.params.commitId;
const db = await getDB();
try {
const commit = await db.collection<TaskCommit>("task_commits").findOne({
_id: new ObjectId(commitId),
taskId,
});
if (!commit) {
ctx.response.status = 404;
ctx.response.body = { error: "Commit link not found" };
return;
}
await db.collection<TaskCommit>("task_commits").deleteOne({ _id: new ObjectId(commitId) });
const now = new Date();
// Changelog-Eintrag
await db.collection("task_changelog").insertOne({
taskId,
field: "commits",
oldValue: `${commit.shortId} - ${commit.commitTitle}`,
newValue: null,
changedBy: ctx.state.user.id,
changedAt: now,
});
// Log-Eintrag
await db.collection("logs").insertOne({
agentId: ctx.state.user.id,
agentName: ctx.state.user.username,
level: "info",
message: `Commit ${commit.shortId} von Task entfernt`,
metadata: { taskId, commitSha: commit.commitSha },
timestamp: now,
});
ctx.response.body = { message: "Commit unlinked" };
} catch {
ctx.response.status = 400;
ctx.response.body = { error: "Invalid ID" };
}
});
// ============ REMINDERS ============
// Set/update reminder for a task
// PATCH /api/tasks/:id/reminder
tasksRouter.patch("/:id/reminder", authMiddleware, async (ctx) => {
const taskId = ctx.params.id;
const body = await ctx.request.body.json();
const { datetime, interval, intervalValue } = body;
if (!datetime) {
ctx.response.status = 400;
ctx.response.body = { error: "datetime is required" };
return;
}
const validIntervals = ['once', 'daily', 'hourly', 'minutes'];
if (interval && !validIntervals.includes(interval)) {
ctx.response.status = 400;
ctx.response.body = { error: `Invalid interval. Must be one of: ${validIntervals.join(', ')}` };
return;
}
const db = await getDB();
const reminder: Reminder = {
enabled: true,
datetime: new Date(datetime),
interval: interval || 'once',
intervalValue: interval === 'minutes' ? (intervalValue || 30) : undefined,
lastNotified: undefined
};
try {
const result = await db.collection<Task>("tasks").updateOne(
{ _id: new ObjectId(taskId) },
{ $set: { reminder, updatedAt: new Date() } }
);
if (result.matchedCount === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Task not found" };
return;
}
ctx.response.body = { message: "Reminder set", reminder };
} catch {
ctx.response.status = 400;
ctx.response.body = { error: "Invalid task ID" };
}
});
// Delete reminder from a task
// DELETE /api/tasks/:id/reminder
tasksRouter.delete("/:id/reminder", authMiddleware, async (ctx) => {
const taskId = ctx.params.id;
const db = await getDB();
try {
const result = await db.collection<Task>("tasks").updateOne(
{ _id: new ObjectId(taskId) },
{ $unset: { reminder: "" }, $set: { updatedAt: new Date() } }
);
if (result.matchedCount === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Task not found" };
return;
}
ctx.response.body = { message: "Reminder removed" };
} catch {
ctx.response.status = 400;
ctx.response.body = { error: "Invalid task ID" };
}
});
// Get due reminders (for cron/polling)
// GET /api/tasks/reminders/due
tasksRouter.get("/reminders/due", authMiddleware, async (ctx) => {
const db = await getDB();
const now = new Date();
// Find all tasks with enabled reminders where datetime <= now
const tasks = await db.collection<Task>("tasks")
.find({
"reminder.enabled": true,
"reminder.datetime": { $lte: now }
})
.toArray();
ctx.response.body = { tasks, count: tasks.length };
});
// Mark reminder as notified and schedule next (for recurring)
// POST /api/tasks/:id/reminder/notified
tasksRouter.post("/:id/reminder/notified", authMiddleware, async (ctx) => {
const taskId = ctx.params.id;
const db = await getDB();
try {
const task = await db.collection<Task>("tasks").findOne({ _id: new ObjectId(taskId) });
if (!task) {
ctx.response.status = 404;
ctx.response.body = { error: "Task not found" };
return;
}
if (!task.reminder) {
ctx.response.status = 400;
ctx.response.body = { error: "Task has no reminder" };
return;
}
const now = new Date();
let nextDatetime: Date | null = null;
// Calculate next reminder time based on interval
switch (task.reminder.interval) {
case 'once':
// Disable after one-time notification
await db.collection<Task>("tasks").updateOne(
{ _id: new ObjectId(taskId) },
{ $set: { "reminder.enabled": false, "reminder.lastNotified": now } }
);
break;
case 'daily':
nextDatetime = new Date(task.reminder.datetime);
nextDatetime.setDate(nextDatetime.getDate() + 1);
break;
case 'hourly':
nextDatetime = new Date(task.reminder.datetime);
nextDatetime.setHours(nextDatetime.getHours() + 1);
break;
case 'minutes':
nextDatetime = new Date(task.reminder.datetime);
nextDatetime.setMinutes(nextDatetime.getMinutes() + (task.reminder.intervalValue || 30));
break;
}
if (nextDatetime) {
await db.collection<Task>("tasks").updateOne(
{ _id: new ObjectId(taskId) },
{ $set: { "reminder.datetime": nextDatetime, "reminder.lastNotified": now } }
);
}
ctx.response.body = {
message: "Reminder marked as notified",
nextDatetime: nextDatetime?.toISOString() || null,
disabled: task.reminder.interval === 'once'
};
} catch {
ctx.response.status = 400;
ctx.response.body = { error: "Invalid task ID" };
}
});
// ============ MIGRATION: Nummernkreise ============
// POST /api/tasks/migrate/numbers - Bestehende Tasks/Agent-Tasks mit Nummern versehen
tasksRouter.post("/migrate/numbers", authMiddleware, async (ctx) => {
if (ctx.state.user.role !== "admin") {
ctx.response.status = 403;
ctx.response.body = { error: "Admin access required" };
return;
}
const db = await getDB();
let taskCount = 0;
let agentTaskCount = 0;
// Tasks ohne Nummer (sortiert nach createdAt)
const tasksWithoutNumber = await db.collection("tasks")
.find({ number: { $exists: false } })
.sort({ createdAt: 1 })
.toArray();
for (const task of tasksWithoutNumber) {
const num = await getNextNumber("task_number");
await db.collection("tasks").updateOne(
{ _id: task._id },
{ $set: { number: num } }
);
taskCount++;
}
// Agent-Tasks ohne Nummer
const agentTasksWithoutNumber = await db.collection("agent_tasks")
.find({ number: { $exists: false } })
.sort({ createdAt: 1 })
.toArray();
for (const at of agentTasksWithoutNumber) {
const num = await getNextNumber("agent_task_number");
await db.collection("agent_tasks").updateOne(
{ _id: at._id },
{ $set: { number: num } }
);
agentTaskCount++;
}
ctx.response.body = {
message: "Migration abgeschlossen",
tasksNumbered: taskCount,
agentTasksNumbered: agentTaskCount,
};
});