import { NextRequest, NextResponse } from 'next/server'; import { createClient } from '@/lib/supabase/server'; import { createAdminClient } from '@/lib/supabase/admin'; import { hybridAI } from '@/lib/ai/hybridRouter'; // ─── Pen name rotation ──────────────────────────────────────────────────────── const BB_PEN_NAMES = [ 'Michael Strand', 'David Harlow', 'Karen Fielding', 'James Mercer', 'Peter Calloway', 'Sandra Voss', 'Brian Kowalski', 'Laura Pennington', 'Thomas Reeves', 'Christine Vale', 'Marcus Webb', 'Ellen Forsythe', ]; const AV_PEN_NAMES = ['Rex Chandler', 'Dana Flux', 'Derek Wainwright', 'Sloane Rigging', 'Chip Crosspoint', 'Blair Presenter', 'Jordan Lumen']; function pickPenName(siteId: number, recentNames: string[]): string { const pool = siteId === 2 ? AV_PEN_NAMES : BB_PEN_NAMES; // Avoid last 3 used names const available = pool.filter(n => !recentNames.slice(-3).includes(n)); return available[Math.floor(Math.random() * available.length)] || pool[0]; } // ─── Company extraction via Hybrid AI ──────────────────────────────────────── async function extractCompanies(title: string, content: string): Promise> { try { const text = `${title}\n\n${content}`.slice(0, 3000); const result = await hybridAI( [ { role: 'system', content: 'Extract all company names, brand names, and product manufacturer names mentioned in this broadcast/AV industry article. Return ONLY a JSON array: [{"name":"Company Name","website_guess":"example.com"}]. Only include real companies. If none found, return [].', }, { role: 'user', content: text }, ], { maxTokens: 500, isBackground: true, priority: 6 } ); const match = result.text.match(/\[[\s\S]*?\]/); if (match) { const parsed = JSON.parse(match[0]); return Array.isArray(parsed) ? parsed.filter((c: any) => c?.name && typeof c.name === 'string') : []; } return []; } catch { return []; } } // ─── Story generation via Hybrid AI ────────────────────────────────────────── async function generateStory(companyName: string, sourceTitle: string, penName: string, siteName: string): Promise<{ title: string; excerpt: string; content: string; confidence: number }> { try { const result = await hybridAI( [ { role: 'system', content: `You are ${penName}, a professional technology journalist writing for ${siteName}. Write an original news article. Rules: write in your own words, lead with the news hook, include relevant context, 300-600 words, professional journalistic tone, do not mention competitor publications, end with a brief company boilerplate paragraph.`, }, { role: 'user', content: `Write a news article about ${companyName} in the ${siteName === 'AV Beat' ? 'professional AV integration' : 'broadcast technology'} industry. This was prompted by their mention in an article titled "${sourceTitle}". Return JSON: {"title":"...","excerpt":"...","content":"

...

","confidence":0.0-1.0}`, }, ], { maxTokens: 1200, isBackground: true, priority: 4 } ); const match = result.text.match(/\{[\s\S]*\}/); if (match) { const parsed = JSON.parse(match[0]); return { title: parsed.title || `${companyName}: Industry Update`, excerpt: parsed.excerpt || '', content: parsed.content || `

Coverage of ${companyName}.

`, confidence: parsed.confidence || 0.7 }; } } catch {} return { title: `${companyName}: Industry Update`, excerpt: `An in-depth look at ${companyName}.`, content: `

This is an AI-drafted story about ${companyName}, prompted by their mention in "${sourceTitle}".

`, confidence: 0.5 }; } // ─── GET ────────────────────────────────────────────────────────────────────── export async function GET(request: NextRequest) { try { const supabase = await createClient(); const { searchParams } = new URL(request.url); const action = searchParams.get('action'); const tab = searchParams.get('tab') || 'queue'; if (action === 'settings') { const [settingsRes, trackerSettingsRes] = await Promise.all([ supabase.from('autopilot_settings').select('*').limit(1).single(), supabase.from('company_tracker_settings').select('*').limit(1).single(), ]); return NextResponse.json({ settings: settingsRes.data || {}, trackerSettings: trackerSettingsRes.data || {} }); } if (tab === 'companies') { const { data, error } = await supabase.from('tracked_companies').select('*').order('mention_count', { ascending: false }); if (error) return NextResponse.json({ error: error.message }, { status: 500 }); return NextResponse.json({ companies: data || [] }); } if (tab === 'crawl-log') { const { data, error } = await supabase.from('crawl_log').select('*').order('crawled_at', { ascending: false }).limit(100); if (error) return NextResponse.json({ error: error.message }, { status: 500 }); return NextResponse.json({ logs: data || [] }); } // Default: story queue const status = searchParams.get('status') || 'pending'; const siteFilter = searchParams.get('site') || ''; let query = supabase.from('auto_story_queue').select('*').order('created_at', { ascending: false }); if (status !== 'all') query = query.eq('status', status); if (siteFilter) query = query.eq('site_id', parseInt(siteFilter)); const { data, error } = await query; if (error) return NextResponse.json({ error: error.message }, { status: 500 }); return NextResponse.json({ queue: data || [] }); } catch (err: any) { return NextResponse.json({ error: err.message }, { status: 500 }); } } // ─── POST ───────────────────────────────────────────────────────────────────── export async function POST(request: NextRequest) { try { // Cron bypass: scheduled scans send x-cron-secret and run with service-role // privileges (RLS bypass) so the scan can read/write admin-protected tables. const cronSecret = request.headers.get('x-cron-secret'); const isCron = !!process.env.CRON_SECRET && cronSecret === process.env.CRON_SECRET; const supabase = isCron ? createAdminClient() : await createClient(); const body = await request.json(); const { action } = body; if (action === 'update_settings') { const { autopilot_enabled, reminder_email, auto_story_global_enabled, max_stories_per_company_per_week, mention_threshold, auto_publish_enabled } = body; const { data: existing } = await supabase.from('autopilot_settings').select('id').limit(1).single(); if (existing?.id) { await supabase.from('autopilot_settings').update({ autopilot_enabled, reminder_email, updated_at: new Date().toISOString() }).eq('id', existing.id); } else { await supabase.from('autopilot_settings').insert({ autopilot_enabled, reminder_email }); } if (auto_story_global_enabled !== undefined || max_stories_per_company_per_week !== undefined || mention_threshold !== undefined || auto_publish_enabled !== undefined) { const { data: ts } = await supabase.from('company_tracker_settings').select('id').limit(1).single(); const trackerUpdate: any = {}; if (auto_story_global_enabled !== undefined) trackerUpdate.auto_story_global_enabled = auto_story_global_enabled; if (max_stories_per_company_per_week !== undefined) trackerUpdate.max_stories_per_company_per_week = max_stories_per_company_per_week; if (mention_threshold !== undefined) trackerUpdate.mention_threshold = mention_threshold; if (auto_publish_enabled !== undefined) trackerUpdate.auto_publish_enabled = auto_publish_enabled; if (ts?.id) await supabase.from('company_tracker_settings').update(trackerUpdate).eq('id', ts.id); } return NextResponse.json({ success: true }); } if (action === 'add_company') { const { company_name, company_website, site_scope, auto_story_enabled, crawl_priority, notes } = body; const { data, error } = await supabase.from('tracked_companies').upsert({ company_name, company_website, site_scope: site_scope || 'both', auto_story_enabled: auto_story_enabled !== false, crawl_priority: crawl_priority || 'standard', notes, }, { onConflict: 'company_name' }).select().single(); if (error) return NextResponse.json({ error: error.message }, { status: 500 }); return NextResponse.json({ company: data }); } if (action === 'force_crawl') { const { company_id } = body; await supabase.from('tracked_companies').update({ next_crawl_scheduled: new Date().toISOString() }).eq('id', company_id); return NextResponse.json({ success: true }); } if (action === 'scan') { const since = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(); const [nativeRes, importedRes] = await Promise.all([ supabase.from('native_articles').select('id, title, content, slug, site_id').eq('status', 'published').gte('published_at', since).order('published_at', { ascending: false }).limit(20), supabase.from('wp_imported_posts').select('id, title, content, wp_slug').eq('status', 'published').gte('wp_published_at', since).order('wp_published_at', { ascending: false }).limit(20), ]); const allArticles = [ ...(nativeRes.data || []).map((a: any) => ({ ...a, source: 'native', site_id: a.site_id || 1 })), ...(importedRes.data || []).map((a: any) => ({ ...a, source: 'imported', site_id: 1 })), ]; const { data: settings } = await supabase.from('company_tracker_settings').select('mention_threshold, auto_story_global_enabled, auto_publish_enabled, auto_publish_confidence_threshold').limit(1).single(); const threshold = settings?.mention_threshold || 2; const autoPilot = settings?.auto_story_global_enabled !== false; const autoPublish = settings?.auto_publish_enabled || false; const confidenceThreshold = settings?.auto_publish_confidence_threshold || 0.85; let newCompaniesFound = 0; const recentPenNames: string[] = []; for (const article of allArticles) { const companies = await extractCompanies(article.title || '', article.content || ''); for (const company of companies) { // Upsert into tracked_companies const { data: existing } = await supabase.from('tracked_companies').select('id, mention_count, auto_story_enabled, site_scope').eq('company_name', company.name).single(); let companyId: string; let mentionCount = 1; let autoStoryEnabled = true; let siteScope = 'both'; if (existing) { mentionCount = (existing.mention_count || 0) + 1; autoStoryEnabled = existing.auto_story_enabled; siteScope = existing.site_scope; companyId = existing.id; await supabase.from('tracked_companies').update({ mention_count: mentionCount, last_mentioned: new Date().toISOString() }).eq('id', companyId); } else { const { data: newCompany } = await supabase.from('tracked_companies').insert({ company_name: company.name, company_website: company.website_guess || null, mention_count: 1, auto_story_enabled: true, site_scope: 'both', }).select('id').single(); companyId = newCompany?.id; newCompaniesFound++; } // Only generate stories for companies meeting threshold if (mentionCount >= threshold && autoStoryEnabled && autoPilot && companyId) { // Check if we already have a recent story queued for this company const { count } = await supabase.from('auto_story_queue').select('id', { count: 'exact', head: true }) .eq('company_id', companyId).gte('created_at', new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString()); if ((count || 0) >= 3) continue; // max 3 per week const siteId = article.site_id || 1; const siteName = siteId === 2 ? 'AV Beat' : 'Broadcast Beat'; const penName = pickPenName(siteId, recentPenNames); recentPenNames.push(penName); const draft = await generateStory(company.name, article.title || '', penName, siteName); if (autoPublish && draft.confidence >= confidenceThreshold) { // Auto-publish const slug = draft.title.toLowerCase().replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-').trim() + '-auto-' + Date.now(); const { data: published } = await supabase.from('native_articles').insert({ slug, title: draft.title, excerpt: draft.excerpt, content: draft.content, author_name: penName, status: 'published', site_id: siteId, published_at: new Date().toISOString(), }).select('id').single(); await supabase.from('auto_story_queue').insert({ company_id: companyId, company_name: company.name, story_type: 'company_news', source_title: article.title, assigned_pen_name: penName, site_id: siteId, status: 'published', generated_title: draft.title, generated_content: draft.content, generated_excerpt: draft.excerpt, quality_confidence: draft.confidence, published_article_id: published?.id, reviewed_at: new Date().toISOString(), }); } else { // Queue for review await supabase.from('auto_story_queue').insert({ company_id: companyId, company_name: company.name, story_type: 'company_news', source_title: article.title, assigned_pen_name: penName, site_id: siteId, status: 'generated', generated_title: draft.title, generated_content: draft.content, generated_excerpt: draft.excerpt, quality_confidence: draft.confidence, }); } } } } // Update autopilot scan stats const { data: apSettings } = await supabase.from('autopilot_settings').select('id').limit(1).single(); if (apSettings?.id) { await supabase.from('autopilot_settings').update({ last_scan_at: new Date().toISOString(), last_scan_articles_processed: allArticles.length, last_scan_companies_found: newCompaniesFound, }).eq('id', apSettings.id); } // Send reminder email if not autopilot const { data: apData } = await supabase.from('autopilot_settings').select('autopilot_enabled, reminder_email').limit(1).single(); if (!apData?.autopilot_enabled && newCompaniesFound > 0) { const { data: queuedItems } = await supabase.from('auto_story_queue').select('company_name').eq('status', 'generated').order('created_at', { ascending: false }).limit(20); const companyNames = [...new Set((queuedItems || []).map((i: any) => i.company_name))]; if (companyNames.length > 0) { fetch('/api/admin/company-coverage-reminder', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ companies: companyNames, count: companyNames.length, email: apData?.reminder_email || 'ryan.salazar@relevantmediaproperties.com' }), }).catch(() => {}); } } return NextResponse.json({ success: true, articlesScanned: allArticles.length, newCompaniesFound }); } return NextResponse.json({ error: 'Unknown action' }, { status: 400 }); } catch (err: any) { return NextResponse.json({ error: err.message }, { status: 500 }); } } // ─── PATCH — approve/reject/reassign story ──────────────────────────────────── export async function PATCH(request: NextRequest) { try { const supabase = await createClient(); const { data: { user }, error: authError } = await supabase.auth.getUser(); if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); const body = await request.json(); const { id, action, pen_name, rejection_reason, site_id } = body; if (action === 'approve') { const { data: item } = await supabase.from('auto_story_queue').select('*').eq('id', id).single(); if (!item) return NextResponse.json({ error: 'Not found' }, { status: 404 }); const slug = (item.generated_title || 'story').toLowerCase().replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-').trim() + '-auto-' + Date.now(); const { data: published } = await supabase.from('native_articles').insert({ slug, title: item.generated_title, excerpt: item.generated_excerpt, content: item.generated_content, author_name: item.assigned_pen_name, status: 'published', site_id: item.site_id, published_at: new Date().toISOString(), }).select('id').single(); await supabase.from('auto_story_queue').update({ status: 'published', reviewed_by: user.id, reviewed_at: new Date().toISOString(), published_article_id: published?.id, }).eq('id', id); return NextResponse.json({ success: true }); } if (action === 'reject') { await supabase.from('auto_story_queue').update({ status: 'rejected', reviewed_by: user.id, reviewed_at: new Date().toISOString(), rejection_reason: rejection_reason || null }).eq('id', id); return NextResponse.json({ success: true }); } if (action === 'reassign_pen_name') { await supabase.from('auto_story_queue').update({ assigned_pen_name: pen_name }).eq('id', id); return NextResponse.json({ success: true }); } if (action === 'reassign_site') { await supabase.from('auto_story_queue').update({ site_id }).eq('id', id); return NextResponse.json({ success: true }); } if (action === 'toggle_company_auto_story') { const { company_id, enabled } = body; await supabase.from('tracked_companies').update({ auto_story_enabled: enabled }).eq('id', company_id); return NextResponse.json({ success: true }); } if (action === 'delete_company') { const { company_id } = body; await supabase.from('tracked_companies').delete().eq('id', company_id); return NextResponse.json({ success: true }); } // Legacy: publish/dismiss from old company_coverage_queue if (action === 'publish' || action === 'dismiss') { const { data: item } = await supabase.from('company_coverage_queue').select('*').eq('id', id).single(); if (!item) return NextResponse.json({ error: 'Not found' }, { status: 404 }); if (action === 'publish') { const slug = (item.ai_draft_title || 'story').toLowerCase().replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-').trim() + '-' + Date.now(); await supabase.from('native_articles').insert({ slug, title: item.ai_draft_title, excerpt: item.ai_draft_excerpt, content: item.ai_draft_content, author_name: 'Editorial Team', status: 'published', published_at: new Date().toISOString() }); await supabase.from('company_coverage_queue').update({ status: 'published' }).eq('id', id); } else { await supabase.from('company_coverage_queue').update({ status: 'dismissed' }).eq('id', id); } return NextResponse.json({ success: true }); } return NextResponse.json({ error: 'Unknown action' }, { status: 400 }); } catch (err: any) { return NextResponse.json({ error: err.message }, { status: 500 }); } }