diff --git a/app/api/activity/route.ts b/app/api/activity/route.ts new file mode 100644 index 00000000..14c3e03c --- /dev/null +++ b/app/api/activity/route.ts @@ -0,0 +1,124 @@ +/** + * GET /api/activity + * + * Workspace-wide activity feed. Aggregates recent events across all of the + * authenticated user's projects: agent sessions (builds), Coolify deployments, + * and project creation/updates. + * + * Returns ActivityItem[] shaped for the workspace Activity page. + */ +import { NextResponse } from 'next/server'; +import { authSession } from '@/lib/auth/session-server'; +import { query } from '@/lib/db-postgres'; + +export const dynamic = 'force-dynamic'; + +interface ActivityItem { + id: string; + projectId: string; + projectName: string; + action: string; + type: 'atlas' | 'build' | 'deploy' | 'user'; + createdAt: string; +} + +export async function GET() { + const session = await authSession(); + if (!session?.user?.email) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const email = session.user.email; + const items: ActivityItem[] = []; + + try { + // --- Agent sessions (build / deploy events) --- + const agentRows = await query( + `SELECT + a.id, + a.project_id, + a.app_name, + a.status, + a.task, + a.created_at, + a.started_at, + a.completed_at, + p.data->>'productName' AS project_name, + p.data->>'name' AS project_name_fallback + FROM agent_sessions a + JOIN fs_projects p ON p.id = a.project_id + WHERE p.user_id = $1 + ORDER BY a.created_at DESC + LIMIT 60`, + [email], + ).catch(() => []); + + for (const r of agentRows) { + const name = r.project_name || r.project_name_fallback || 'Untitled'; + const status = r.status as string; + const type: ActivityItem['type'] = + status === 'completed' || status === 'failed' ? 'deploy' : 'build'; + const verb = + status === 'completed' + ? 'Deployed' + : status === 'failed' + ? 'Deploy failed for' + : status === 'running' + ? 'Building' + : 'Queued build for'; + items.push({ + id: `agent-${r.id}`, + projectId: r.project_id, + projectName: name, + action: `${verb} ${r.app_name || 'app'}`, + type, + createdAt: (r.completed_at || r.started_at || r.created_at).toISOString(), + }); + } + + // --- Project creation / significant updates --- + const projectRows = await query( + `SELECT id, data, created_at, updated_at + FROM fs_projects + WHERE user_id = $1 + ORDER BY created_at DESC + LIMIT 40`, + [email], + ).catch(() => []); + + for (const r of projectRows) { + const name = r.data?.productName || r.data?.name || 'Untitled'; + items.push({ + id: `project-created-${r.id}`, + projectId: r.id, + projectName: name, + action: `Created project "${name}"`, + type: 'user', + createdAt: r.created_at.toISOString(), + }); + + // If there's a notable status change in data, surface it + const status = r.data?.status; + if (status && status !== 'defining') { + items.push({ + id: `project-status-${r.id}`, + projectId: r.id, + projectName: name, + action: `Project moved to "${status}"`, + type: 'atlas', + createdAt: r.updated_at.toISOString(), + }); + } + } + + // Sort all items by date descending, cap at 80 + items.sort( + (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), + ); + + return NextResponse.json({ items: items.slice(0, 80) }); + } catch (err) { + console.error('[/api/activity]', err); + return NextResponse.json({ items: [] }); + } +}