The workspace Activity page (/[workspace]/activity) was calling /api/activity which did not exist, so the feed was always empty. New route aggregates agent_sessions (builds/deploys) and fs_projects (creation/status changes) across all user projects, returning ActivityItem[] sorted by date descending. Made-with: Cursor
125 lines
3.5 KiB
TypeScript
125 lines
3.5 KiB
TypeScript
/**
|
|
* 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<any>(
|
|
`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<any>(
|
|
`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: [] });
|
|
}
|
|
}
|