feat: Implement analysis job tracking with progress timeline and enhanced data source status management.

This commit is contained in:
2026-02-03 22:43:27 +00:00
parent c47614bc66
commit 358f2a42dd
22 changed files with 2251 additions and 219 deletions

View File

@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'
import { convexAuthNextjsToken, isAuthenticatedNextjs } from "@convex-dev/auth/nextjs/server";
import { fetchQuery } from "convex/nextjs";
import { fetchMutation, fetchQuery } from "convex/nextjs";
import { api } from "@/convex/_generated/api";
import { z } from 'zod'
import { generateSearchQueries, getDefaultPlatforms } from '@/lib/query-generator'
@@ -9,13 +9,14 @@ import type { EnhancedProductAnalysis, SearchConfig, PlatformConfig } from '@/li
const searchSchema = z.object({
projectId: z.string(),
jobId: z.optional(z.string()),
config: z.object({
platforms: z.array(z.object({
id: z.string(),
name: z.string(),
icon: z.string(),
icon: z.string().optional(),
enabled: z.boolean(),
searchTemplate: z.string(),
searchTemplate: z.string().optional(),
rateLimit: z.number()
})),
strategies: z.array(z.string()),
@@ -25,6 +26,7 @@ const searchSchema = z.object({
})
export async function POST(request: NextRequest) {
let jobId: string | undefined
try {
if (!(await isAuthenticatedNextjs())) {
const redirectUrl = new URL("/auth", request.url);
@@ -35,9 +37,18 @@ export async function POST(request: NextRequest) {
}
const body = await request.json()
const { projectId, config } = searchSchema.parse(body)
const parsed = searchSchema.parse(body)
const { projectId, config } = parsed
jobId = parsed.jobId
const token = await convexAuthNextjsToken();
if (jobId) {
await fetchMutation(
api.searchJobs.update,
{ jobId: jobId as any, status: "running", progress: 10 },
{ token }
);
}
const searchContext = await fetchQuery(
api.projects.getSearchContext,
{ projectId: projectId as any },
@@ -45,6 +56,13 @@ export async function POST(request: NextRequest) {
);
if (!searchContext.context) {
if (jobId) {
await fetchMutation(
api.searchJobs.update,
{ jobId: jobId as any, status: "failed", error: "No analysis available." },
{ token }
);
}
return NextResponse.json(
{ error: 'No analysis available for selected sources.' },
{ status: 400 }
@@ -60,18 +78,51 @@ export async function POST(request: NextRequest) {
// Generate queries
console.log(' Generating search queries...')
const queries = generateSearchQueries(analysis as EnhancedProductAnalysis, config as SearchConfig)
const enforcedConfig: SearchConfig = {
...(config as SearchConfig),
maxResults: Math.min((config as SearchConfig).maxResults || 50, 50),
}
const queries = generateSearchQueries(analysis as EnhancedProductAnalysis, enforcedConfig)
console.log(` ✓ Generated ${queries.length} queries`)
if (jobId) {
await fetchMutation(
api.searchJobs.update,
{ jobId: jobId as any, status: "running", progress: 40 },
{ token }
);
}
// Execute searches
console.log(' Executing searches...')
const searchResults = await executeSearches(queries)
console.log(` ✓ Found ${searchResults.length} raw results`)
if (jobId) {
await fetchMutation(
api.searchJobs.update,
{ jobId: jobId as any, status: "running", progress: 70 },
{ token }
);
}
// Score and rank
console.log(' Scoring opportunities...')
const opportunities = scoreOpportunities(searchResults, analysis as EnhancedProductAnalysis)
console.log(` ✓ Scored ${opportunities.length} opportunities`)
if (jobId) {
await fetchMutation(
api.searchJobs.update,
{ jobId: jobId as any, status: "running", progress: 90 },
{ token }
);
}
if (jobId) {
await fetchMutation(
api.searchJobs.update,
{ jobId: jobId as any, status: "completed", progress: 100 },
{ token }
);
}
return NextResponse.json({
success: true,
@@ -97,17 +148,36 @@ export async function POST(request: NextRequest) {
})
} catch (error: any) {
console.error('❌ Opportunity search error:', error)
const errorMessage =
error instanceof Error ? error.message : typeof error === "string" ? error : "Search failed"
console.error("❌ Opportunity search error:", errorMessage)
if (jobId) {
try {
const token = await convexAuthNextjsToken();
await fetchMutation(
api.searchJobs.update,
{
jobId: jobId as any,
status: "failed",
error: errorMessage
},
{ token }
);
} catch {
// Best-effort job update only.
}
}
if (error.name === 'ZodError') {
if (error?.name === 'ZodError') {
return NextResponse.json(
{ error: 'Invalid request format', details: error.errors },
{ error: 'Invalid request format', details: error?.errors },
{ status: 400 }
)
}
return NextResponse.json(
{ error: error.message || 'Failed to search for opportunities' },
{ error: errorMessage || 'Failed to search for opportunities' },
{ status: 500 }
)
}