Files
net/frontend/src/components/admin/content/ContentFactoryDashboard.tsx

245 lines
13 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { getDirectusClient, readItems, aggregate } from '@/lib/directus/client';
import type { DirectusSchema, GenerationJobs as GenerationJob, CampaignMasters as CampaignMaster, WorkLog } from '@/lib/schemas';
export default function ContentFactoryDashboard() {
const [stats, setStats] = useState({ total: 0, published: 0, processing: 0 });
const [jobs, setJobs] = useState<GenerationJob[]>([]);
const [campaigns, setCampaigns] = useState<CampaignMaster[]>([]);
const [logs, setLogs] = useState<WorkLog[]>([]);
const [loading, setLoading] = useState(true);
const DIRECTUS_ADMIN_URL = "https://spark.jumpstartscaling.com/admin";
useEffect(() => {
loadData();
const interval = setInterval(loadData, 5000); // Poll every 5s for "Factory" feel
return () => clearInterval(interval);
}, []);
const loadData = async () => {
try {
const client = getDirectusClient();
// 1. Fetch KPI Stats
// Article Count
const articleAgg = await client.request(aggregate('generated_articles', {
aggregate: { count: '*' }
}));
const totalArticles = Number(articleAgg[0]?.count || 0);
// Published Count
const publishedAgg = await client.request(aggregate('generated_articles', {
aggregate: { count: '*' },
filter: { is_published: { _eq: true } }
}));
const totalPublished = Number(publishedAgg[0]?.count || 0);
// Active Jobs Count
const processingAgg = await client.request(aggregate('generation_jobs', {
aggregate: { count: '*' },
filter: { status: { _eq: 'Processing' } }
}));
const totalProcessing = Number(processingAgg[0]?.count || 0);
setStats({
total: totalArticles,
published: totalPublished,
processing: totalProcessing
});
// 2. Fetch Active Campaigns
const activeCampaigns = await client.request(readItems('campaign_masters', {
limit: 5,
sort: ['-date_created'],
filter: { status: { _in: ['active', 'paused'] } } // Show active/paused
}));
setCampaigns(activeCampaigns as unknown as CampaignMaster[]);
// 3. Fetch Production Jobs (The real "Factory" work)
const recentJobs = await client.request(readItems('generation_jobs', {
limit: 5,
sort: ['-date_created']
}));
setJobs(recentJobs as unknown as GenerationJob[]);
// 4. Fetch Work Log
const recentLogs = await client.request(readItems('work_log', {
limit: 20,
sort: ['-date_created']
}));
setLogs(recentLogs as unknown as WorkLog[]);
setLoading(false);
} catch (error) {
console.error("Dashboard Load Error:", error);
setLoading(false);
}
};
const StatusBadge = ({ status }: { status: string }) => {
const colors: Record<string, string> = {
active: 'bg-green-600',
paused: 'bg-yellow-600',
completed: 'bg-blue-600',
draft: 'bg-slate-600',
Pending: 'bg-slate-600',
Processing: 'bg-blue-600',
Complete: 'bg-green-600',
Failed: 'bg-red-600'
};
return <Badge className={`${colors[status] || 'bg-slate-600'} text-white`}>{status}</Badge>;
};
if (loading) return <div className="text-white p-8 animate-pulse">Initializing Factory Command Center...</div>;
return (
<div className="space-y-8">
{/* Header Actions */}
<div className="flex justify-between items-center">
<div>
<h2 className="text-xl font-bold text-white">Tactical Command Center</h2>
<p className="text-slate-400">Shields (SEO Defense) & Weapons (Content Offense) Status</p>
</div>
<div className="flex gap-4">
<Button variant="outline" className="text-slate-200 border-slate-700 hover:bg-slate-800" onClick={() => window.open(`${DIRECTUS_ADMIN_URL}/content/posts`, '_blank')}>
Manage Arsenal (Posts)
</Button>
<Button className="bg-blue-600 hover:bg-blue-700" onClick={() => window.open(`${DIRECTUS_ADMIN_URL}`, '_blank')}>
Open HQ (Directus)
</Button>
</div>
</div>
{/* KPI Grid */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="bg-slate-900 border-slate-800">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-slate-400">Total Units (Articles)</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-white">{stats.total}</div>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800 border-l-4 border-l-purple-500">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-slate-400">Pending QA</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-white">{stats.total - stats.published}</div>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800 border-l-4 border-l-green-500">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-slate-400">Deployed (Live)</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-white">{stats.published}</div>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800 border-l-4 border-l-blue-500">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-slate-400">Active Operations</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-white">{stats.processing}</div>
</CardContent>
</Card>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Active Campaigns */}
<Card className="bg-slate-900 border-slate-800 lg:col-span-2">
<CardHeader>
<CardTitle className="text-white flex justify-between">
<span>Active Campaigns</span>
<Button variant="ghost" size="sm" onClick={() => window.open(`${DIRECTUS_ADMIN_URL}/content/campaign_masters`, '_blank')}>View All</Button>
</CardTitle>
<CardDescription>Recent campaign activity and status</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow className="border-slate-800 hover:bg-slate-900/50">
<TableHead className="text-slate-400">Campaign Name</TableHead>
<TableHead className="text-slate-400">Mode</TableHead>
<TableHead className="text-slate-400">Status</TableHead>
<TableHead className="text-right text-slate-400">Target</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{campaigns.length > 0 ? campaigns.map((campaign) => (
<TableRow key={campaign.id} className="border-slate-800 hover:bg-slate-800/50">
<TableCell className="font-medium text-white">{campaign.name}</TableCell>
<TableCell className="text-slate-400">{campaign.location_mode || 'Standard'}</TableCell>
<TableCell><StatusBadge status={campaign.status} /></TableCell>
<TableCell className="text-right text-slate-400">{campaign.batch_count || 0}</TableCell>
</TableRow>
)) : (
<TableRow>
<TableCell colSpan={4} className="text-center text-slate-500 py-8">No active campaigns</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</CardContent>
</Card>
{/* Production Queue */}
<Card className="bg-slate-900 border-slate-800">
<CardHeader>
<CardTitle className="text-white">Production Jobs</CardTitle>
<CardDescription>Recent generation tasks</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{jobs.length > 0 ? jobs.map((job) => (
<div key={job.id} className="bg-slate-950 p-4 rounded border border-slate-800 flex justify-between items-center">
<div>
<div className="text-sm font-medium text-white mb-1">Job #{String(job.id)}</div>
<div className="text-xs text-slate-500">
{job.current_offset} / {job.target_quantity} articles
</div>
</div>
<StatusBadge status={job.status} />
</div>
)) : (
<div className="text-center text-slate-500 py-8">Queue is empty</div>
)}
<Button className="w-full bg-slate-800 hover:bg-slate-700 text-white border border-slate-700" onClick={() => window.open('/admin/sites/jumpstart', '_blank')}>
+ Start Refactor Job
</Button>
</div>
</CardContent>
</Card>
</div>
{/* Work Log / Activity */}
<Card className="bg-slate-900 border-slate-800">
<CardHeader>
<CardTitle className="text-white">System Activity Log</CardTitle>
<CardDescription>Real-time backend operations</CardDescription>
</CardHeader>
<CardContent>
<div className="bg-black rounded-lg p-4 font-mono text-sm h-64 overflow-y-auto border border-slate-800">
{logs.length > 0 ? logs.map((log) => (
<div key={log.id} className="mb-2 border-b border-slate-900 pb-2 last:border-0">
<span className="text-slate-500">[{new Date(log.date_created || '').toLocaleTimeString()}]</span>{' '}
<span className={log.action === 'create' ? 'text-green-400' : 'text-blue-400'}>{(log.action || 'INFO').toUpperCase()}</span>{' '}
<span className="text-slate-300">{log.entity_type} #{log.entity_id}</span>{' '}
<span className="text-slate-600">- {typeof log.details === 'string' ? log.details.substring(0, 50) : JSON.stringify(log.details || '').substring(0, 50)}...</span>
</div>
)) : (
<div className="text-slate-600 text-center mt-8">No recent activity</div>
)}
</div>
</CardContent>
</Card>
</div>
);
}