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 { 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"; import { getAuthenticatedUser } from "@/lib/server/auth";
export const runtime = "nodejs"; 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 // GET - fetch all tasks, projects, and sprints
export async function GET() { export async function GET() {
try { try {
@ -11,15 +38,29 @@ export async function GET() {
if (!user) { if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 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) { } 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 }); 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) { export async function POST(request: Request) {
try { try {
const user = await getAuthenticatedUser(); const user = await getAuthenticatedUser();
@ -28,70 +69,83 @@ export async function POST(request: Request) {
} }
const body = await request.json(); const body = await request.json();
const { task, tasks, projects, sprints } = body as { const { task } = body as { task?: Task };
task?: Task;
tasks?: Task[]; if (!task) {
projects?: DataStore["projects"]; return NextResponse.json({ error: "Missing task" }, { status: 400 });
sprints?: DataStore["sprints"]; }
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(); let result;
if (existing) {
if (projects) data.projects = projects; // Update existing
if (sprints) data.sprints = sprints; const { data, error } = await supabase
.from("tasks")
if (task) { .update(taskData)
const existingIndex = data.tasks.findIndex((t) => t.id === task.id); .eq("id", task.id)
if (existingIndex >= 0) { .select()
const existingTask = data.tasks[existingIndex]; .single();
data.tasks[existingIndex] = { if (error) throw error;
...existingTask, result = data;
...task, } else {
updatedAt: new Date().toISOString(), // Insert new
updatedById: user.id, const { data, error } = await supabase
updatedByName: user.name, .from("tasks")
updatedByAvatarUrl: user.avatarUrl, .insert(taskData)
}; .select()
} else { .single();
data.tasks.push({ if (error) throw error;
...task, result = data;
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,
});
}
} }
if (tasks && Array.isArray(tasks)) { // Update lastUpdated
data.tasks = tasks.map((entry) => ({ await supabase.from("meta").upsert({
...entry, key: "lastUpdated",
createdById: entry.createdById || user.id, value: String(Date.now()),
createdByName: entry.createdByName || user.name, updated_at: now,
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,
}));
}
const saved = await saveData(data); return NextResponse.json({ success: true, task: result });
return NextResponse.json({ success: true, data: saved }); } catch (error: unknown) {
} catch (error) { console.error(">>> API POST error:", error);
console.error(">>> API POST: database error:", error); const message = error instanceof Error ? error.message : "Failed to save";
return NextResponse.json({ error: "Failed to save" }, { status: 500 }); 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 { id } = (await request.json()) as { id: string };
const data = await getData();
data.tasks = data.tasks.filter((t) => t.id !== id); const supabase = getServiceSupabase();
await saveData(data); 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 }); return NextResponse.json({ success: true });
} catch (error) { } catch (error: unknown) {
console.error(">>> API DELETE: database error:", error); console.error(">>> API DELETE error:", error);
return NextResponse.json({ error: "Failed to delete" }, { status: 500 }); 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), replies: removeCommentFromThread(normalizeComments(comment.replies), targetId),
})) }))
// Helper to sync to server // Helper to sync a single task to server (lightweight)
async function syncToServer(projects: Project[], tasks: Task[], sprints: Sprint[]) { async function syncTaskToServer(task: Task) {
console.log('>>> syncToServer: saving', tasks.length, 'tasks,', projects.length, 'projects,', sprints.length, 'sprints') console.log('>>> syncTaskToServer: saving task', task.id, task.title)
const t2 = tasks.find(t => t.id === '2')
if (t2) console.log('>>> syncToServer: task 2 sprintId:', t2.sprintId)
try { 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', { const res = await fetch('/api/tasks', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: body, body: JSON.stringify({ task }),
}) })
if (res.ok) { if (res.ok) {
const responseData = await res.json() console.log('>>> syncTaskToServer: saved successfully')
console.log('>>> syncToServer: saved successfully, server now has', responseData.data?.tasks?.length, 'tasks') 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 { } else {
console.error('>>> syncToServer: failed with status', res.status) console.error('>>> syncToServer: failed with status', res.status)
} }
} catch (error) { } 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) => { set((state) => {
const newTasks = [...state.tasks, newTask] const newTasks = [...state.tasks, newTask]
syncToServer(state.projects, newTasks, state.sprints) // Sync individual task to server (lightweight)
syncTaskToServer(newTask)
return { tasks: newTasks } return { tasks: newTasks }
}) })
}, },
@ -724,7 +765,10 @@ export const useTaskStore = create<TaskStore>()(
) )
const updatedTask = newTasks.find(t => t.id === id) const updatedTask = newTasks.find(t => t.id === id)
console.log('updateTask: updated task:', updatedTask) 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 } return { tasks: newTasks }
}) })
}, },
@ -732,7 +776,8 @@ export const useTaskStore = create<TaskStore>()(
deleteTask: (id) => { deleteTask: (id) => {
set((state) => { set((state) => {
const newTasks = state.tasks.filter((t) => t.id !== id) const newTasks = state.tasks.filter((t) => t.id !== id)
syncToServer(state.projects, newTasks, state.sprints) // Delete individual task from server (lightweight)
deleteTaskFromServer(id)
return { return {
tasks: newTasks, tasks: newTasks,
selectedTaskId: state.selectedTaskId === id ? null : state.selectedTaskId, selectedTaskId: state.selectedTaskId === id ? null : state.selectedTaskId,
@ -810,7 +855,10 @@ export const useTaskStore = create<TaskStore>()(
} }
: t : t
) )
syncToServer(state.projects, newTasks, state.sprints) const updatedTask = newTasks.find(t => t.id === taskId)
if (updatedTask) {
syncTaskToServer(updatedTask)
}
return { tasks: newTasks } return { tasks: newTasks }
}) })
}, },
@ -830,7 +878,10 @@ export const useTaskStore = create<TaskStore>()(
} }
: t : t
) )
syncToServer(state.projects, newTasks, state.sprints) const updatedTask = newTasks.find(t => t.id === taskId)
if (updatedTask) {
syncTaskToServer(updatedTask)
}
return { tasks: newTasks } return { tasks: newTasks }
}) })
}, },