'use client'; import React, { useState, useEffect, useCallback } from 'react'; import Link from 'next/link'; import { useAuth } from '@/contexts/AuthContext'; import { useRouter } from 'next/navigation'; // ─── Types ──────────────────────────────────────────────────────────────────── interface NodeStatus { name: string; online: boolean; queueDepth: number; latencyMs: number; host: string; port: number; } interface PipelineStatus { generatedToday: number; siteBreakdown: Record; queueDepth: number; pressReleasesRemaining: number; thinContentRemaining: number; missingSeoRemaining: number; nodes: NodeStatus[]; timestamp: string; } interface AuditSummary { totalArticles: number; totalPublished: number; thinContentCount: number; missingFeaturedImageCount: number; missingSeoAnyCount: number; pressReleaseCount: number; averageWordCount: number; } interface LaunchChecklist { zeroPagesWithMissingFeaturedImages: boolean; zeroPagesWithThinContent: boolean; zeroPagesMissingSeoMeta: boolean; zeroUnrewrittenPressReleases: boolean; } // ─── Site labels ────────────────────────────────────────────────────────────── const SITE_LABELS: Record = { 'broadcast-beat': { label: 'AV Beat', color: 'text-blue-400', target: '15–25/day' }, 'av-beat': { label: 'AV Beat', color: 'text-purple-400', target: '10–15/day' }, 'backlot-beat': { label: 'Backlot Beat', color: 'text-amber-400', target: '10–15/day' }, 'broadcast-engineering': { label: 'BroadcastEngineering.com', color: 'text-green-400', target: '10–15/day' }, }; // ─── Sub-components ─────────────────────────────────────────────────────────── function NodeCard({ node }: { node: NodeStatus }) { return (
{node.name} {node.online ? 'ONLINE' : 'OFFLINE'}

{node.host}:{node.port}

{node.online && (

Queue

10 ? 'text-amber-400' : 'text-green-400'}`}> {node.queueDepth === 999 ? '—' : node.queueDepth}

Latency

{node.latencyMs}ms

)}
); } function StatCard({ label, value, sub, accent, good, }: { label: string; value: number | string; sub?: string; accent: string; good?: boolean; }) { return (

{value}

{label}

{sub &&

{sub}

} {good !== undefined && ( {good ? '✓ Launch ready' : '⚠ Action needed'} )}
); } function ChecklistItem({ label, done }: { label: string; done: boolean }) { return (
{done ? '✓' : '○'} {label}
); } // ─── Generate Controls ──────────────────────────────────────────────────────── type RMPSite = 'broadcast-beat' | 'av-beat' | 'backlot-beat' | 'broadcast-engineering'; function GeneratePanel({ onGenerated }: { onGenerated: () => void }) { const [site, setSite] = useState('broadcast-beat'); const [topic, setTopic] = useState(''); const [loading, setLoading] = useState(false); const [result, setResult] = useState(null); const [error, setError] = useState(null); const handleGenerate = async () => { setLoading(true); setResult(null); setError(null); try { const res = await fetch('/api/content/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ site, topic: topic.trim() || undefined }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Generation failed'); setResult(`✓ Generated: "${data.article?.title}" via ${data.article?.node} (${data.article?.wordCount} words)`); onGenerated(); } catch (e: any) { setError(e.message); } finally { setLoading(false); } }; const handleSEOBatch = async () => { setLoading(true); setResult(null); setError(null); try { const res = await fetch('/api/content/seo-batch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ site }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'SEO batch failed'); setResult(`✓ SEO meta updated: ${data.updated} articles. Remaining: ${data.remaining}`); onGenerated(); } catch (e: any) { setError(e.message); } finally { setLoading(false); } }; return (

Manual Controls

setTopic(e.target.value)} placeholder="e.g. ATSC 3.0 deployment update" className="w-full bg-[#1a1a1a] border border-[#333] rounded-lg px-3 py-2 text-sm text-white font-body placeholder-[#444] focus:outline-none focus:border-[#3b82f6]" />
{result &&

{result}

} {error &&

{error}

}
); } // ─── Main Page ──────────────────────────────────────────────────────────────── export default function ContentStatusPage() { const { user, loading } = useAuth(); const router = useRouter(); const [status, setStatus] = useState(null); const [audit, setAudit] = useState(null); const [checklist, setChecklist] = useState(null); const [loadingData, setLoadingData] = useState(true); const [lastRefresh, setLastRefresh] = useState(null); const [autoRefresh, setAutoRefresh] = useState(true); const fetchAll = useCallback(async () => { try { const [statusRes, auditRes] = await Promise.all([ fetch('/api/content/pipeline-status'), fetch('/api/content/audit'), ]); if (statusRes.ok) { const s = await statusRes.json(); setStatus(s); } if (auditRes.ok) { const a = await auditRes.json(); setAudit(a.summary); setChecklist(a.launchChecklist); } setLastRefresh(new Date()); } catch { // silent } finally { setLoadingData(false); } }, []); useEffect(() => { if (!loading && !user) router.push('/login?next=' + encodeURIComponent(window.location.pathname)); }, [user, loading, router]); useEffect(() => { if (user) fetchAll(); }, [user, fetchAll]); // Auto-refresh every 30 seconds useEffect(() => { if (!autoRefresh || !user) return; const interval = setInterval(fetchAll, 30_000); return () => clearInterval(interval); }, [autoRefresh, user, fetchAll]); if (loading || loadingData) { return (

Loading content status…

); } if (!user) return null; const allNodesOffline = status?.nodes.every((n) => !n.online) ?? false; const launchReady = checklist ? Object.values(checklist).every(Boolean) : false; return (
{/* Header */}
← Admin /

Content Pipeline Status

{allNodesOffline && ( ALL NODES OFFLINE )}
{lastRefresh && ( Updated {lastRefresh.toLocaleTimeString()} )}
{/* AI Node Status */}

AI Node Status

{(status?.nodes || [ { name: 'AI_001', online: false, queueDepth: 0, latencyMs: 0, host: '68.248.192.64', port: 8765 }, { name: 'AI_002', online: false, queueDepth: 0, latencyMs: 0, host: '68.248.192.64', port: 8766 }, ]).map((node) => ( ))}
{allNodesOffline && (

⚠ Both AI nodes are offline. New generation jobs are being queued and will retry automatically every 5 minutes.

)}
{/* Today's Pipeline */}

Today's Pipeline

10 ? 'text-amber-400' : 'text-green-400'} sub="Jobs pending across nodes" /> 0 ? 'text-amber-400' : 'text-green-400'} good={status?.pressReleasesRemaining === 0} /> 0 ? 'text-amber-400' : 'text-green-400'} good={status?.missingSeoRemaining === 0} />
{/* Per-Site Breakdown */}

Per-Site Breakdown (Today)

{Object.entries(SITE_LABELS).map(([key, cfg]) => { const count = status?.siteBreakdown?.[key] ?? 0; return (

{cfg.label}

{count}

Target: {cfg.target}

); })}
{/* Content Audit Summary */} {audit && (

Content Audit

0 ? 'text-amber-400' : 'text-green-400'} good={audit.thinContentCount === 0} /> 0 ? 'text-amber-400' : 'text-green-400'} good={audit.missingFeaturedImageCount === 0} /> 0 ? 'text-amber-400' : 'text-green-400'} good={audit.missingSeoAnyCount === 0} />
)} {/* Launch Checklist */}

Launch Checklist

{launchReady && ( LAUNCH READY )}
0} />
{/* Manual Controls */}

Manual Controls

{/* API Reference */}

API Endpoints

{[ { method: 'GET', path: '/api/content/audit', desc: 'Full content audit report' }, { method: 'GET', path: '/api/content/pipeline-status', desc: 'Live pipeline + node status' }, { method: 'POST', path: '/api/content/generate', desc: 'Generate one article { site, topic? }' }, { method: 'GET', path: '/api/content/press-releases', desc: 'List unedited press releases' }, { method: 'POST', path: '/api/content/press-releases', desc: 'Rewrite a press release { articleId, source, site }' }, { method: 'POST', path: '/api/content/seo-batch', desc: 'Batch-generate missing SEO meta { site? }' }, { method: 'GET', path: '/api/ai/node-health', desc: 'AI_001 node health check' }, { method: 'GET', path: '/api/ai/ollama-health', desc: 'AI_001 ping check' }, ].map((ep) => (
{ep.method}
{ep.path}

{ep.desc}

))}
); }