import { NextRequest, NextResponse } from 'next/server' import { requireRole } from '@/lib/auth' import { getDatabase } from '@/lib/db' import { heavyLimiter } from '@/lib/rate-limit' interface SearchResult { type: 'task' | 'agent' | 'activity' | 'audit' | 'message' | 'notification' | 'webhook' | 'pipeline' id: number title: string subtitle?: string excerpt?: string created_at: number relevance: number } /** * GET /api/search?q=&type=&limit= * Global search across all MC entities. */ export async function GET(request: NextRequest) { const auth = requireRole(request, 'viewer') if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) const rateCheck = heavyLimiter(request) if (rateCheck) return rateCheck const { searchParams } = new URL(request.url) const query = searchParams.get('q')?.trim() const typeFilter = searchParams.get('type') const limit = Math.min(parseInt(searchParams.get('limit') || '30'), 100) if (!query || query.length < 2) { return NextResponse.json({ error: 'Query must be at least 2 characters' }, { status: 400 }) } const db = getDatabase() const workspaceId = auth.user.workspace_id ?? 1 const likeQ = `%${query}%` const results: SearchResult[] = [] // Search tasks if (!typeFilter || typeFilter === 'task') { try { const tasks = db.prepare(` SELECT id, title, description, status, assigned_to, created_at FROM tasks WHERE workspace_id = ? AND (title LIKE ? OR description LIKE ? OR assigned_to LIKE ?) ORDER BY created_at DESC LIMIT ? `).all(workspaceId, likeQ, likeQ, likeQ, limit) as any[] for (const t of tasks) { results.push({ type: 'task', id: t.id, title: t.title, subtitle: `${t.status} ${t.assigned_to ? `· ${t.assigned_to}` : ''}`, excerpt: truncateMatch(t.description, query), created_at: t.created_at, relevance: t.title.toLowerCase().includes(query.toLowerCase()) ? 2 : 1, }) } } catch { /* table might not exist */ } } // Search agents if (!typeFilter || typeFilter === 'agent') { try { const agents = db.prepare(` SELECT id, name, role, status, last_activity, created_at FROM agents WHERE workspace_id = ? AND (name LIKE ? OR role LIKE ? OR last_activity LIKE ?) ORDER BY created_at DESC LIMIT ? `).all(workspaceId, likeQ, likeQ, likeQ, limit) as any[] for (const a of agents) { results.push({ type: 'agent', id: a.id, title: a.name, subtitle: `${a.role} · ${a.status}`, excerpt: a.last_activity, created_at: a.created_at, relevance: a.name.toLowerCase().includes(query.toLowerCase()) ? 2 : 1, }) } } catch { /* table might not exist */ } } // Search activities if (!typeFilter || typeFilter === 'activity') { try { const activities = db.prepare(` SELECT id, type, actor, description, created_at FROM activities WHERE workspace_id = ? AND (description LIKE ? OR actor LIKE ?) ORDER BY created_at DESC LIMIT ? `).all(workspaceId, likeQ, likeQ, limit) as any[] for (const a of activities) { results.push({ type: 'activity', id: a.id, title: a.description, subtitle: `by ${a.actor}`, created_at: a.created_at, relevance: 1, }) } } catch { /* table might not exist */ } } // Search audit log (admin-only — audit_log is instance-global) if ((!typeFilter || typeFilter === 'audit') && auth.user.role === 'admin') { try { const audits = db.prepare(` SELECT id, action, actor, detail, created_at FROM audit_log WHERE action LIKE ? OR actor LIKE ? OR detail LIKE ? ORDER BY created_at DESC LIMIT ? `).all(likeQ, likeQ, likeQ, limit) as any[] for (const a of audits) { results.push({ type: 'audit', id: a.id, title: a.action, subtitle: `by ${a.actor}`, excerpt: truncateMatch(a.detail, query), created_at: a.created_at, relevance: 1, }) } } catch { /* table might not exist */ } } // Search messages if (!typeFilter || typeFilter === 'message') { try { const messages = db.prepare(` SELECT id, from_agent, to_agent, content, conversation_id, created_at FROM messages WHERE workspace_id = ? AND (content LIKE ? OR from_agent LIKE ?) ORDER BY created_at DESC LIMIT ? `).all(workspaceId, likeQ, likeQ, limit) as any[] for (const m of messages) { results.push({ type: 'message', id: m.id, title: `${m.from_agent} → ${m.to_agent || 'all'}`, subtitle: m.conversation_id, excerpt: truncateMatch(m.content, query), created_at: m.created_at, relevance: 1, }) } } catch { /* table might not exist */ } } // Search webhooks if (!typeFilter || typeFilter === 'webhook') { try { const webhooks = db.prepare(` SELECT id, name, url, events, created_at FROM webhooks WHERE workspace_id = ? AND (name LIKE ? OR url LIKE ?) ORDER BY created_at DESC LIMIT ? `).all(workspaceId, likeQ, likeQ, limit) as any[] for (const w of webhooks) { results.push({ type: 'webhook', id: w.id, title: w.name, subtitle: w.url, created_at: w.created_at, relevance: w.name.toLowerCase().includes(query.toLowerCase()) ? 2 : 1, }) } } catch { /* table might not exist */ } } // Search pipelines if (!typeFilter || typeFilter === 'pipeline') { try { const pipelines = db.prepare(` SELECT id, name, description, created_at FROM workflow_pipelines WHERE workspace_id = ? AND (name LIKE ? OR description LIKE ?) ORDER BY created_at DESC LIMIT ? `).all(workspaceId, likeQ, likeQ, limit) as any[] for (const p of pipelines) { results.push({ type: 'pipeline', id: p.id, title: p.name, excerpt: truncateMatch(p.description, query), created_at: p.created_at, relevance: p.name.toLowerCase().includes(query.toLowerCase()) ? 2 : 1, }) } } catch { /* table might not exist */ } } // Sort by relevance then recency results.sort((a, b) => b.relevance - a.relevance || b.created_at - a.created_at) return NextResponse.json({ query, count: results.length, results: results.slice(0, limit), }) } function truncateMatch(text: string | null, query: string, maxLen = 120): string | undefined { if (!text) return undefined const lower = text.toLowerCase() const idx = lower.indexOf(query.toLowerCase()) if (idx === -1) return text.slice(0, maxLen) + (text.length > maxLen ? '...' : '') const start = Math.max(0, idx - 40) const end = Math.min(text.length, idx + query.length + 80) const excerpt = (start > 0 ? '...' : '') + text.slice(start, end) + (end < text.length ? '...' : '') return excerpt }