Files
avbeat-com/src/app/api/admin/company-coverage/route.ts

354 lines
20 KiB
TypeScript

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<Array<{ name: string; website_guess: string }>> {
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":"<p>...</p>","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 || `<p>Coverage of ${companyName}.</p>`, confidence: parsed.confidence || 0.7 };
}
} catch {}
return { title: `${companyName}: Industry Update`, excerpt: `An in-depth look at ${companyName}.`, content: `<p>This is an AI-drafted story about ${companyName}, prompted by their mention in "${sourceTitle}".</p>`, 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 });
}
}