Fix 413 error - send individual tasks instead of bulk

This commit is contained in:
OpenClaw Bot 2026-02-21 15:47:05 -06:00
parent b3015e8bb8
commit 5b7efa2c18
2 changed files with 202 additions and 86 deletions

View File

@ -1,9 +1,36 @@
import { NextResponse } from "next/server";
import { getData, saveData, type DataStore, type Task } from "@/lib/server/taskDb";
import { getServiceSupabase } from "@/lib/supabase/client";
import { getAuthenticatedUser } from "@/lib/server/auth";
export const runtime = "nodejs";
interface Task {
id: string;
title: string;
description?: string;
type: "idea" | "task" | "bug" | "research" | "plan";
status: "open" | "todo" | "blocked" | "in-progress" | "review" | "validate" | "archived" | "canceled" | "done";
priority: "low" | "medium" | "high" | "urgent";
projectId: string;
sprintId?: string;
createdAt?: string;
updatedAt?: string;
createdById?: string;
createdByName?: string;
createdByAvatarUrl?: string;
updatedById?: string;
updatedByName?: string;
updatedByAvatarUrl?: string;
assigneeId?: string;
assigneeName?: string;
assigneeEmail?: string;
assigneeAvatarUrl?: string;
dueDate?: string;
comments?: unknown[];
tags?: string[];
attachments?: unknown[];
}
// GET - fetch all tasks, projects, and sprints
export async function GET() {
try {
@ -11,15 +38,29 @@ export async function GET() {
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const data = await getData();
return NextResponse.json(data);
const supabase = getServiceSupabase();
const [{ data: projects }, { data: sprints }, { data: tasks }, { data: meta }] = await Promise.all([
supabase.from("projects").select("*").order("created_at", { ascending: true }),
supabase.from("sprints").select("*").order("start_date", { ascending: true }),
supabase.from("tasks").select("*").order("created_at", { ascending: true }),
supabase.from("meta").select("value").eq("key", "lastUpdated").maybeSingle(),
]);
return NextResponse.json({
projects: projects || [],
sprints: sprints || [],
tasks: tasks || [],
lastUpdated: Number(meta?.value ?? Date.now()),
});
} catch (error) {
console.error(">>> API GET: database error:", error);
console.error(">>> API GET error:", error);
return NextResponse.json({ error: "Failed to fetch data" }, { status: 500 });
}
}
// POST - create or update tasks, projects, or sprints
// POST - create or update a single task (lightweight)
export async function POST(request: Request) {
try {
const user = await getAuthenticatedUser();
@ -28,70 +69,83 @@ export async function POST(request: Request) {
}
const body = await request.json();
const { task, tasks, projects, sprints } = body as {
task?: Task;
tasks?: Task[];
projects?: DataStore["projects"];
sprints?: DataStore["sprints"];
const { task } = body as { task?: Task };
if (!task) {
return NextResponse.json({ error: "Missing task" }, { status: 400 });
}
const supabase = getServiceSupabase();
const now = new Date().toISOString();
// Check if task exists
const { data: existing } = await supabase
.from("tasks")
.select("id")
.eq("id", task.id)
.maybeSingle();
const taskData = {
id: task.id,
title: task.title,
description: task.description || null,
type: task.type || "task",
status: task.status || "todo",
priority: task.priority || "medium",
project_id: task.projectId,
sprint_id: task.sprintId || null,
created_at: existing ? undefined : (task.createdAt || now),
updated_at: now,
created_by_id: existing ? undefined : (task.createdById || user.id),
created_by_name: existing ? undefined : (task.createdByName || user.name),
created_by_avatar_url: existing ? undefined : task.createdByAvatarUrl,
updated_by_id: user.id,
updated_by_name: user.name,
updated_by_avatar_url: user.avatarUrl,
assignee_id: task.assigneeId || null,
assignee_name: task.assigneeName || null,
assignee_email: task.assigneeEmail || null,
assignee_avatar_url: task.assigneeAvatarUrl || null,
due_date: task.dueDate || null,
comments: task.comments || [],
tags: task.tags || [],
attachments: task.attachments || [],
};
const data = await getData();
if (projects) data.projects = projects;
if (sprints) data.sprints = sprints;
if (task) {
const existingIndex = data.tasks.findIndex((t) => t.id === task.id);
if (existingIndex >= 0) {
const existingTask = data.tasks[existingIndex];
data.tasks[existingIndex] = {
...existingTask,
...task,
updatedAt: new Date().toISOString(),
updatedById: user.id,
updatedByName: user.name,
updatedByAvatarUrl: user.avatarUrl,
};
} else {
data.tasks.push({
...task,
id: task.id || Date.now().toString(),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
createdById: task.createdById || user.id,
createdByName: task.createdByName || user.name,
createdByAvatarUrl: task.createdByAvatarUrl || user.avatarUrl,
updatedById: user.id,
updatedByName: user.name,
updatedByAvatarUrl: user.avatarUrl,
assigneeId: task.assigneeId || user.id,
assigneeName: task.assigneeName || user.name,
assigneeEmail: task.assigneeEmail || user.email,
});
}
let result;
if (existing) {
// Update existing
const { data, error } = await supabase
.from("tasks")
.update(taskData)
.eq("id", task.id)
.select()
.single();
if (error) throw error;
result = data;
} else {
// Insert new
const { data, error } = await supabase
.from("tasks")
.insert(taskData)
.select()
.single();
if (error) throw error;
result = data;
}
if (tasks && Array.isArray(tasks)) {
data.tasks = tasks.map((entry) => ({
...entry,
createdById: entry.createdById || user.id,
createdByName: entry.createdByName || user.name,
createdByAvatarUrl: entry.createdByAvatarUrl || (entry.createdById === user.id ? user.avatarUrl : undefined),
updatedById: entry.updatedById || user.id,
updatedByName: entry.updatedByName || user.name,
updatedByAvatarUrl: entry.updatedByAvatarUrl || (entry.updatedById === user.id ? user.avatarUrl : undefined),
assigneeId: entry.assigneeId || undefined,
assigneeName: entry.assigneeName || undefined,
assigneeEmail: entry.assigneeEmail || undefined,
assigneeAvatarUrl: undefined,
}));
}
// Update lastUpdated
await supabase.from("meta").upsert({
key: "lastUpdated",
value: String(Date.now()),
updated_at: now,
});
const saved = await saveData(data);
return NextResponse.json({ success: true, data: saved });
} catch (error) {
console.error(">>> API POST: database error:", error);
return NextResponse.json({ error: "Failed to save" }, { status: 500 });
return NextResponse.json({ success: true, task: result });
} catch (error: unknown) {
console.error(">>> API POST error:", error);
const message = error instanceof Error ? error.message : "Failed to save";
return NextResponse.json({ error: message }, { status: 500 });
}
}
@ -104,12 +158,23 @@ export async function DELETE(request: Request) {
}
const { id } = (await request.json()) as { id: string };
const data = await getData();
data.tasks = data.tasks.filter((t) => t.id !== id);
await saveData(data);
const supabase = getServiceSupabase();
const { error } = await supabase.from("tasks").delete().eq("id", id);
if (error) throw error;
// Update lastUpdated
await supabase.from("meta").upsert({
key: "lastUpdated",
value: String(Date.now()),
updated_at: new Date().toISOString(),
});
return NextResponse.json({ success: true });
} catch (error) {
console.error(">>> API DELETE: database error:", error);
return NextResponse.json({ error: "Failed to delete" }, { status: 500 });
} catch (error: unknown) {
console.error(">>> API DELETE error:", error);
const message = error instanceof Error ? error.message : "Failed to delete";
return NextResponse.json({ error: message }, { status: 500 });
}
}

View File

@ -552,27 +552,67 @@ const removeCommentFromThread = (comments: Comment[], targetId: string): Comment
replies: removeCommentFromThread(normalizeComments(comment.replies), targetId),
}))
// Helper to sync to server
async function syncToServer(projects: Project[], tasks: Task[], sprints: Sprint[]) {
console.log('>>> syncToServer: saving', tasks.length, 'tasks,', projects.length, 'projects,', sprints.length, 'sprints')
const t2 = tasks.find(t => t.id === '2')
if (t2) console.log('>>> syncToServer: task 2 sprintId:', t2.sprintId)
// Helper to sync a single task to server (lightweight)
async function syncTaskToServer(task: Task) {
console.log('>>> syncTaskToServer: saving task', task.id, task.title)
try {
const body = JSON.stringify({ projects, tasks, sprints })
console.log('>>> syncToServer: sending body with keys:', Object.keys(JSON.parse(body)))
const res = await fetch('/api/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: body,
body: JSON.stringify({ task }),
})
if (res.ok) {
const responseData = await res.json()
console.log('>>> syncToServer: saved successfully, server now has', responseData.data?.tasks?.length, 'tasks')
console.log('>>> syncTaskToServer: saved successfully')
return true
} else {
console.error('>>> syncTaskToServer: failed with status', res.status)
return false
}
} catch (error) {
console.error('>>> syncTaskToServer: Failed to sync:', error)
return false
}
}
// Helper to delete a task from server
async function deleteTaskFromServer(taskId: string) {
console.log('>>> deleteTaskFromServer: deleting task', taskId)
try {
const res = await fetch('/api/tasks', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: taskId }),
})
if (res.ok) {
console.log('>>> deleteTaskFromServer: deleted successfully')
return true
} else {
console.error('>>> deleteTaskFromServer: failed with status', res.status)
return false
}
} catch (error) {
console.error('>>> deleteTaskFromServer: Failed to delete:', error)
return false
}
}
// Legacy bulk sync (for projects/sprints only)
async function syncToServer(projects: Project[], tasks: Task[], sprints: Sprint[]) {
console.log('>>> syncToServer: legacy bulk sync - projects/sprints only')
// Only sync projects and sprints in bulk, tasks are handled individually
try {
const res = await fetch('/api/projects', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ projects, sprints }),
})
if (res.ok) {
console.log('>>> syncToServer: saved projects/sprints successfully')
} else {
console.error('>>> syncToServer: failed with status', res.status)
}
} catch (error) {
console.error('>>> syncToServer: Failed to sync to server:', error)
console.error('>>> syncToServer: Failed to sync:', error)
}
}
@ -699,7 +739,8 @@ export const useTaskStore = create<TaskStore>()(
}
set((state) => {
const newTasks = [...state.tasks, newTask]
syncToServer(state.projects, newTasks, state.sprints)
// Sync individual task to server (lightweight)
syncTaskToServer(newTask)
return { tasks: newTasks }
})
},
@ -724,7 +765,10 @@ export const useTaskStore = create<TaskStore>()(
)
const updatedTask = newTasks.find(t => t.id === id)
console.log('updateTask: updated task:', updatedTask)
syncToServer(state.projects, newTasks, state.sprints)
// Sync individual task to server (lightweight)
if (updatedTask) {
syncTaskToServer(updatedTask)
}
return { tasks: newTasks }
})
},
@ -732,7 +776,8 @@ export const useTaskStore = create<TaskStore>()(
deleteTask: (id) => {
set((state) => {
const newTasks = state.tasks.filter((t) => t.id !== id)
syncToServer(state.projects, newTasks, state.sprints)
// Delete individual task from server (lightweight)
deleteTaskFromServer(id)
return {
tasks: newTasks,
selectedTaskId: state.selectedTaskId === id ? null : state.selectedTaskId,
@ -810,7 +855,10 @@ export const useTaskStore = create<TaskStore>()(
}
: t
)
syncToServer(state.projects, newTasks, state.sprints)
const updatedTask = newTasks.find(t => t.id === taskId)
if (updatedTask) {
syncTaskToServer(updatedTask)
}
return { tasks: newTasks }
})
},
@ -830,7 +878,10 @@ export const useTaskStore = create<TaskStore>()(
}
: t
)
syncToServer(state.projects, newTasks, state.sprints)
const updatedTask = newTasks.find(t => t.id === taskId)
if (updatedTask) {
syncTaskToServer(updatedTask)
}
return { tasks: newTasks }
})
},