diff --git a/app/api/tasks/[taskId]/client-logs/route.ts b/app/api/tasks/[taskId]/client-logs/route.ts
new file mode 100644
index 00000000..7fe979f2
--- /dev/null
+++ b/app/api/tasks/[taskId]/client-logs/route.ts
@@ -0,0 +1,66 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { z } from 'zod'
+import { db } from '@/lib/db/client'
+import * as schema from '@/lib/db/schema'
+import { eq, and, isNull } from 'drizzle-orm'
+import { getServerSession } from '@/lib/session/get-server-session'
+import { redactSensitiveInfo } from '@/lib/utils/logging'
+
+const { tasks, logEntrySchema } = schema
+
+// Schema for the request body
+const clientLogsSchema = z.object({
+ logs: z.array(logEntrySchema),
+})
+
+/**
+ * POST /api/tasks/[taskId]/client-logs
+ * Append client-side logs to the task's log database
+ */
+export async function POST(request: NextRequest, { params }: { params: Promise<{ taskId: string }> }) {
+ try {
+ const session = await getServerSession()
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ }
+
+ const { taskId } = await params
+
+ // Parse the request body
+ const body = await request.json()
+ const { logs } = clientLogsSchema.parse(body)
+
+ if (!logs || logs.length === 0) {
+ return NextResponse.json({ error: 'No logs provided' }, { status: 400 })
+ }
+
+ // Get the task and verify ownership (exclude soft-deleted)
+ const [task] = await db
+ .select()
+ .from(tasks)
+ .where(and(eq(tasks.id, taskId), eq(tasks.userId, session.user.id), isNull(tasks.deletedAt)))
+ .limit(1)
+
+ if (!task) {
+ return NextResponse.json({ error: 'Task not found' }, { status: 404 })
+ }
+
+ // Redact sensitive information from all log messages
+ const sanitizedLogs = logs.map((log) => ({
+ ...log,
+ message: redactSensitiveInfo(log.message),
+ timestamp: log.timestamp || new Date(),
+ }))
+
+ // Append the client logs to the existing logs
+ const existingLogs = task.logs || []
+ const updatedLogs = [...existingLogs, ...sanitizedLogs]
+
+ await db.update(tasks).set({ logs: updatedLogs, updatedAt: new Date() }).where(eq(tasks.id, taskId))
+
+ return NextResponse.json({ success: true })
+ } catch (error) {
+ console.error('Error appending client logs:', error)
+ return NextResponse.json({ error: 'Failed to append client logs' }, { status: 500 })
+ }
+}
diff --git a/components/logs-pane.tsx b/components/logs-pane.tsx
index e036417c..7da47487 100644
--- a/components/logs-pane.tsx
+++ b/components/logs-pane.tsx
@@ -17,7 +17,7 @@ interface LogsPaneProps {
}
type TabType = 'logs' | 'terminal'
-type LogFilterType = 'all' | 'platform' | 'server'
+type LogFilterType = 'all' | 'platform' | 'server' | 'client'
export function LogsPane({ task, onHeightChange }: LogsPaneProps) {
const [copiedLogs, setCopiedLogs] = useState(false)
@@ -161,8 +161,10 @@ export function LogsPane({ task, onHeightChange }: LogsPaneProps) {
const getFilteredLogs = (filter: LogFilterType) => {
return (task.logs || []).filter((log) => {
const isServerLog = log.message.startsWith('[SERVER]')
+ const isClientLog = log.message.startsWith('[CLIENT]')
if (filter === 'server') return isServerLog
- if (filter === 'platform') return !isServerLog
+ if (filter === 'client') return isClientLog
+ if (filter === 'platform') return !isServerLog && !isClientLog
return true
})
}
@@ -302,6 +304,7 @@ export function LogsPane({ task, onHeightChange }: LogsPaneProps) {
All
Platform
Server
+ Client