alot of othe mess fro autoamtion overview an ddetiaeld run apge sonly
This commit is contained in:
@@ -1,150 +1,139 @@
|
||||
/**
|
||||
* Automation Overview Page
|
||||
* Comprehensive dashboard showing automation status, metrics, cost estimation, and run history
|
||||
* Meaningful dashboard showing actual production data, not estimates
|
||||
*/
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import { useSiteStore } from '../../store/siteStore';
|
||||
import { automationService } from '../../services/automationService';
|
||||
import { OverviewStatsResponse } from '../../types/automation';
|
||||
import {
|
||||
fetchKeywords,
|
||||
fetchClusters,
|
||||
fetchContentIdeas,
|
||||
fetchTasks,
|
||||
fetchContent,
|
||||
fetchImages,
|
||||
} from '../../services/api';
|
||||
import RunHistory from '../../components/Automation/RunHistory';
|
||||
import PageMeta from '../../components/common/PageMeta';
|
||||
import PageHeader from '../../components/common/PageHeader';
|
||||
import ComponentCard from '../../components/common/ComponentCard';
|
||||
import RunStatisticsSummary from '../../components/Automation/DetailView/RunStatisticsSummary';
|
||||
import PredictiveCostAnalysis from '../../components/Automation/DetailView/PredictiveCostAnalysis';
|
||||
import AttentionItemsAlert from '../../components/Automation/DetailView/AttentionItemsAlert';
|
||||
import EnhancedRunHistory from '../../components/Automation/DetailView/EnhancedRunHistory';
|
||||
import MeaningfulRunHistory from '../../components/Automation/DetailView/MeaningfulRunHistory';
|
||||
import ProductionSummary from '../../components/Automation/DetailView/ProductionSummary';
|
||||
import {
|
||||
ListIcon,
|
||||
GroupIcon,
|
||||
BoltIcon,
|
||||
ClockIcon,
|
||||
FileTextIcon,
|
||||
FileIcon,
|
||||
BoltIcon,
|
||||
PaperPlaneIcon,
|
||||
} from '../../icons';
|
||||
|
||||
interface ActualCounts {
|
||||
keywords: number;
|
||||
clusters: number;
|
||||
ideas: number;
|
||||
tasks: number;
|
||||
content: number;
|
||||
images: number;
|
||||
}
|
||||
|
||||
interface AutomationTotals {
|
||||
total_runs: number;
|
||||
runs_with_output: number;
|
||||
total_credits: number;
|
||||
clusters_created: number;
|
||||
ideas_created: number;
|
||||
content_created: number;
|
||||
images_created: number;
|
||||
approved_via_automation: number;
|
||||
clusters_total: number;
|
||||
ideas_total: number;
|
||||
content_total: number;
|
||||
images_total: number;
|
||||
}
|
||||
|
||||
interface Efficiency {
|
||||
total_items_created: number;
|
||||
credits_per_item: number;
|
||||
}
|
||||
|
||||
interface MeaningfulRun {
|
||||
run_id: string;
|
||||
run_number: number;
|
||||
status: string;
|
||||
started_at: string;
|
||||
duration_seconds: number;
|
||||
total_credits: number;
|
||||
stages: Array<{
|
||||
stage: number;
|
||||
name: string;
|
||||
input: number;
|
||||
output: number;
|
||||
credits: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface ProductionStats {
|
||||
totals: AutomationTotals;
|
||||
actual_counts: ActualCounts;
|
||||
efficiency: Efficiency;
|
||||
meaningful_runs: MeaningfulRun[];
|
||||
}
|
||||
|
||||
const AutomationOverview: React.FC = () => {
|
||||
const { activeSite } = useSiteStore();
|
||||
const navigate = useNavigate();
|
||||
const toast = useToast();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [metrics, setMetrics] = useState<any>(null);
|
||||
const [overviewStats, setOverviewStats] = useState<OverviewStatsResponse | null>(null);
|
||||
const [historyPage, setHistoryPage] = useState(1);
|
||||
const [historyData, setHistoryData] = useState<any>(null);
|
||||
const [productionStats, setProductionStats] = useState<ProductionStats | null>(null);
|
||||
const [hasRunning, setHasRunning] = useState(false);
|
||||
const [pendingCounts, setPendingCounts] = useState({
|
||||
keywords: 0,
|
||||
content: 0,
|
||||
images: 0,
|
||||
review: 0,
|
||||
});
|
||||
|
||||
// Load metrics for the 5 metric cards
|
||||
const loadMetrics = async () => {
|
||||
const loadData = async () => {
|
||||
if (!activeSite) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const [
|
||||
keywordsTotalRes, keywordsNewRes, keywordsMappedRes,
|
||||
clustersTotalRes, clustersNewRes, clustersMappedRes,
|
||||
ideasTotalRes, ideasNewRes, ideasQueuedRes, ideasCompletedRes,
|
||||
tasksTotalRes,
|
||||
contentTotalRes, contentDraftRes, contentReviewRes, contentPublishedRes,
|
||||
contentNotPublishedRes, contentScheduledRes,
|
||||
imagesTotalRes, imagesPendingRes,
|
||||
] = await Promise.all([
|
||||
fetchKeywords({ page_size: 1, site_id: activeSite.id }),
|
||||
fetchKeywords({ page_size: 1, site_id: activeSite.id, status: 'new' }),
|
||||
fetchKeywords({ page_size: 1, site_id: activeSite.id, status: 'mapped' }),
|
||||
fetchClusters({ page_size: 1, site_id: activeSite.id }),
|
||||
fetchClusters({ page_size: 1, site_id: activeSite.id, status: 'new' }),
|
||||
fetchClusters({ page_size: 1, site_id: activeSite.id, status: 'mapped' }),
|
||||
fetchContentIdeas({ page_size: 1, site_id: activeSite.id }),
|
||||
fetchContentIdeas({ page_size: 1, site_id: activeSite.id, status: 'new' }),
|
||||
fetchContentIdeas({ page_size: 1, site_id: activeSite.id, status: 'queued' }),
|
||||
fetchContentIdeas({ page_size: 1, site_id: activeSite.id, status: 'completed' }),
|
||||
fetchTasks({ page_size: 1, site_id: activeSite.id }),
|
||||
fetchContent({ page_size: 1, site_id: activeSite.id }),
|
||||
fetchContent({ page_size: 1, site_id: activeSite.id, status: 'draft' }),
|
||||
fetchContent({ page_size: 1, site_id: activeSite.id, status: 'review' }),
|
||||
fetchContent({ page_size: 1, site_id: activeSite.id, status__in: 'approved,published' }),
|
||||
fetchContent({ page_size: 1, site_id: activeSite.id, status: 'approved' }),
|
||||
fetchContent({ page_size: 1, site_id: activeSite.id, status: 'approved' }),
|
||||
fetchImages({ page_size: 1 }),
|
||||
fetchImages({ page_size: 1, status: 'pending' }),
|
||||
const [stats, currentRun, pipeline] = await Promise.all([
|
||||
automationService.getProductionStats(activeSite.id),
|
||||
automationService.getCurrentRun(activeSite.id),
|
||||
automationService.getPipelineOverview(activeSite.id),
|
||||
]);
|
||||
|
||||
setMetrics({
|
||||
keywords: { total: keywordsTotalRes.count || 0, new: keywordsNewRes.count || 0, mapped: keywordsMappedRes.count || 0 },
|
||||
clusters: { total: clustersTotalRes.count || 0, new: clustersNewRes.count || 0, mapped: clustersMappedRes.count || 0 },
|
||||
ideas: { total: ideasTotalRes.count || 0, new: ideasNewRes.count || 0, queued: ideasQueuedRes.count || 0, completed: ideasCompletedRes.count || 0 },
|
||||
tasks: { total: tasksTotalRes.count || 0 },
|
||||
content: {
|
||||
total: contentTotalRes.count || 0,
|
||||
draft: contentDraftRes.count || 0,
|
||||
review: contentReviewRes.count || 0,
|
||||
published: contentPublishedRes.count || 0,
|
||||
not_published: contentNotPublishedRes.count || 0,
|
||||
scheduled: contentScheduledRes.count || 0,
|
||||
},
|
||||
images: { total: imagesTotalRes.count || 0, pending: imagesPendingRes.count || 0 },
|
||||
});
|
||||
setProductionStats(stats);
|
||||
setHasRunning(!!currentRun.run && (currentRun.run.status === 'running' || currentRun.run.status === 'paused'));
|
||||
|
||||
// Extract pending counts from pipeline
|
||||
if (pipeline.stages) {
|
||||
const stage1 = pipeline.stages.find((s: any) => s.number === 1);
|
||||
const stage5 = pipeline.stages.find((s: any) => s.number === 5);
|
||||
const stage6 = pipeline.stages.find((s: any) => s.number === 6);
|
||||
const stage7 = pipeline.stages.find((s: any) => s.number === 7);
|
||||
setPendingCounts({
|
||||
keywords: stage1?.pending || 0,
|
||||
content: stage5?.pending || 0,
|
||||
images: stage6?.pending || 0,
|
||||
review: stage7?.pending || 0,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to fetch metrics for automation overview', e);
|
||||
}
|
||||
};
|
||||
|
||||
// Load cost estimate
|
||||
const loadOverviewStats = async () => {
|
||||
if (!activeSite) return;
|
||||
|
||||
try {
|
||||
const stats = await automationService.getOverviewStats(activeSite.id);
|
||||
setOverviewStats(stats);
|
||||
} catch (e) {
|
||||
console.warn('Failed to fetch overview stats', e);
|
||||
}
|
||||
};
|
||||
|
||||
// Load enhanced history
|
||||
const loadEnhancedHistory = async (page: number = 1) => {
|
||||
if (!activeSite) return;
|
||||
|
||||
try {
|
||||
const history = await automationService.getEnhancedHistory(activeSite.id, page, 10);
|
||||
setHistoryData(history);
|
||||
} catch (e) {
|
||||
console.warn('Failed to fetch enhanced history', e);
|
||||
// Set to null so fallback component shows
|
||||
setHistoryData(null);
|
||||
console.error('Failed to load production stats', e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
await Promise.all([loadMetrics(), loadOverviewStats(), loadEnhancedHistory(historyPage)]);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
if (activeSite) {
|
||||
loadData();
|
||||
}
|
||||
}, [activeSite, historyPage]);
|
||||
}, [activeSite]);
|
||||
|
||||
// Helper to render metric rows
|
||||
const renderMetricRow = (items: Array<{ label: string; value: number; colorCls: string }>) => {
|
||||
return (
|
||||
<div className="flex justify-between text-xs mt-2">
|
||||
{items.map((item, idx) => (
|
||||
<div key={idx} className="flex items-baseline gap-1">
|
||||
<span className="text-gray-500 dark:text-gray-400">{item.label}</span>
|
||||
<span className={`font-semibold ${item.colorCls}`}>{item.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
const handleStartRun = async () => {
|
||||
if (!activeSite) return;
|
||||
|
||||
try {
|
||||
await automationService.runNow(activeSite.id);
|
||||
toast.success('Automation started');
|
||||
navigate('/automation');
|
||||
} catch (e: any) {
|
||||
toast.error(e?.message || 'Failed to start automation');
|
||||
}
|
||||
};
|
||||
|
||||
if (!activeSite) {
|
||||
@@ -158,166 +147,120 @@ const AutomationOverview: React.FC = () => {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<div className="text-gray-500 dark:text-gray-400">Loading automation overview...</div>
|
||||
<div className="text-gray-500 dark:text-gray-400">Loading automation data...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const totals = productionStats?.totals || {
|
||||
total_runs: 0,
|
||||
runs_with_output: 0,
|
||||
total_credits: 0,
|
||||
clusters_created: 0,
|
||||
ideas_created: 0,
|
||||
content_created: 0,
|
||||
images_created: 0,
|
||||
approved_via_automation: 0,
|
||||
clusters_total: 0,
|
||||
ideas_total: 0,
|
||||
content_total: 0,
|
||||
images_total: 0,
|
||||
};
|
||||
|
||||
const actual_counts = productionStats?.actual_counts || {
|
||||
keywords: 0,
|
||||
clusters: 0,
|
||||
ideas: 0,
|
||||
tasks: 0,
|
||||
content: 0,
|
||||
images: 0,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta title="Automation Overview" description="Comprehensive automation dashboard" />
|
||||
<PageMeta title="Automation Overview" description="Production dashboard" />
|
||||
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title="Automation Overview"
|
||||
breadcrumb="Automation / Overview"
|
||||
description="Comprehensive automation dashboard with metrics, cost estimation, and run history"
|
||||
description="Your content production metrics and history"
|
||||
/>
|
||||
|
||||
{/* Metrics Summary Cards */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||
{/* Keywords */}
|
||||
<div className="bg-white dark:bg-gray-900 rounded-xl p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="size-8 rounded-lg bg-brand-100 dark:bg-brand-900/30 flex items-center justify-center">
|
||||
<ListIcon className="size-4 text-brand-600 dark:text-brand-400" />
|
||||
</div>
|
||||
<div className="text-base font-bold text-gray-900 dark:text-white">Keywords</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-3xl font-bold text-brand-600">{metrics?.keywords?.total || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
{renderMetricRow([
|
||||
{ label: 'New:', value: metrics?.keywords?.new || 0, colorCls: 'text-brand-600' },
|
||||
{ label: 'Mapped:', value: metrics?.keywords?.mapped || 0, colorCls: 'text-brand-600' },
|
||||
])}
|
||||
</div>
|
||||
{/* Quick Actions Row - Compact */}
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={hasRunning ? () => navigate('/automation') : handleStartRun}
|
||||
className={`
|
||||
flex items-center gap-2 px-4 py-2 rounded-lg font-medium transition-all
|
||||
${hasRunning
|
||||
? 'bg-brand-100 text-brand-700 dark:bg-brand-900/30 dark:text-brand-400'
|
||||
: 'bg-brand-600 text-white hover:bg-brand-700'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<BoltIcon className="w-4 h-4" />
|
||||
{hasRunning ? 'View Running' : 'Start Run'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => navigate('/automation')}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg font-medium bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition-all"
|
||||
>
|
||||
<ClockIcon className="w-4 h-4" />
|
||||
Schedule
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => navigate('/writer/content')}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg font-medium bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition-all"
|
||||
>
|
||||
<FileTextIcon className="w-4 h-4" />
|
||||
Content
|
||||
{pendingCounts.content > 0 && (
|
||||
<span className="ml-1 px-1.5 py-0.5 text-xs rounded-full bg-warning-500 text-white">
|
||||
{pendingCounts.content}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => navigate('/writer/content?status=review')}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg font-medium bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition-all"
|
||||
>
|
||||
<PaperPlaneIcon className="w-4 h-4" />
|
||||
Review
|
||||
{pendingCounts.review > 0 && (
|
||||
<span className="ml-1 px-1.5 py-0.5 text-xs rounded-full bg-success-500 text-white">
|
||||
{pendingCounts.review}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Clusters */}
|
||||
<div className="bg-white dark:bg-gray-900 rounded-xl p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="size-8 rounded-lg bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center">
|
||||
<GroupIcon className="size-4 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<div className="text-base font-bold text-gray-900 dark:text-white">Clusters</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-3xl font-bold text-purple-600">{metrics?.clusters?.total || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
{renderMetricRow([
|
||||
{ label: 'New:', value: metrics?.clusters?.new || 0, colorCls: 'text-purple-600' },
|
||||
{ label: 'Mapped:', value: metrics?.clusters?.mapped || 0, colorCls: 'text-purple-600' },
|
||||
])}
|
||||
</div>
|
||||
|
||||
{/* Ideas */}
|
||||
<div className="bg-white dark:bg-gray-900 rounded-xl p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="size-8 rounded-lg bg-warning-100 dark:bg-warning-900/30 flex items-center justify-center">
|
||||
<BoltIcon className="size-4 text-warning-600 dark:text-warning-400" />
|
||||
</div>
|
||||
<div className="text-base font-bold text-gray-900 dark:text-white">Ideas</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-3xl font-bold text-warning-600">{metrics?.ideas?.total || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
{renderMetricRow([
|
||||
{ label: 'New:', value: metrics?.ideas?.new || 0, colorCls: 'text-warning-600' },
|
||||
{ label: 'Queued:', value: metrics?.ideas?.queued || 0, colorCls: 'text-warning-600' },
|
||||
{ label: 'Done:', value: metrics?.ideas?.completed || 0, colorCls: 'text-warning-600' },
|
||||
])}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="bg-white dark:bg-gray-900 rounded-xl p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="size-8 rounded-lg bg-success-100 dark:bg-success-900/30 flex items-center justify-center">
|
||||
<FileTextIcon className="size-4 text-success-600 dark:text-success-400" />
|
||||
</div>
|
||||
<div className="text-base font-bold text-gray-900 dark:text-white">Content</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-3xl font-bold text-success-600">{metrics?.content?.total || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
{renderMetricRow([
|
||||
{ label: 'Draft:', value: metrics?.content?.draft || 0, colorCls: 'text-success-600' },
|
||||
{ label: 'Review:', value: metrics?.content?.review || 0, colorCls: 'text-success-600' },
|
||||
{ label: 'Publish:', value: metrics?.content?.published || 0, colorCls: 'text-success-600' },
|
||||
])}
|
||||
</div>
|
||||
|
||||
{/* Images */}
|
||||
<div className="bg-white dark:bg-gray-900 rounded-xl p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="size-8 rounded-lg bg-info-100 dark:bg-info-900/30 flex items-center justify-center">
|
||||
<FileIcon className="size-4 text-info-600 dark:text-info-400" />
|
||||
</div>
|
||||
<div className="text-base font-bold text-gray-900 dark:text-white">Images</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-3xl font-bold text-info-600">{metrics?.images?.total || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
{renderMetricRow([
|
||||
{ label: 'Pending:', value: metrics?.images?.pending || 0, colorCls: 'text-info-600' },
|
||||
])}
|
||||
</div>
|
||||
{/* Pipeline ready indicator */}
|
||||
{(pendingCounts.keywords > 0 || pendingCounts.images > 0) && (
|
||||
<span className="ml-auto text-sm text-gray-500 dark:text-gray-400">
|
||||
Pipeline: {pendingCounts.keywords > 0 && `${pendingCounts.keywords} keywords`}
|
||||
{pendingCounts.keywords > 0 && pendingCounts.images > 0 && ', '}
|
||||
{pendingCounts.images > 0 && `${pendingCounts.images} pending images`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Cost Estimation Card */}
|
||||
{overviewStats ? (
|
||||
<>
|
||||
{/* Attention Items Alert */}
|
||||
{overviewStats.attention_items && (
|
||||
<AttentionItemsAlert items={overviewStats.attention_items} />
|
||||
)}
|
||||
{/* Production Summary - now uses actual_counts */}
|
||||
<ProductionSummary
|
||||
totals={totals}
|
||||
actual_counts={actual_counts}
|
||||
efficiency={productionStats?.efficiency}
|
||||
loading={loading}
|
||||
/>
|
||||
|
||||
{/* Statistics and Predictive Analysis */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{overviewStats.run_statistics && (
|
||||
<RunStatisticsSummary statistics={overviewStats.run_statistics} loading={loading} />
|
||||
)}
|
||||
{overviewStats.predictive_analysis && (
|
||||
<PredictiveCostAnalysis analysis={overviewStats.predictive_analysis} loading={loading} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : !loading && (
|
||||
<div className="bg-white dark:bg-gray-900 rounded-xl p-6">
|
||||
<p className="text-gray-600 dark:text-gray-400">Loading automation statistics...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Enhanced Run History */}
|
||||
{historyData && historyData.runs && (
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">Run History</h2>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Click on any run to view detailed analysis
|
||||
</p>
|
||||
</div>
|
||||
<EnhancedRunHistory
|
||||
runs={historyData.runs}
|
||||
loading={loading}
|
||||
currentPage={historyData.pagination?.page || 1}
|
||||
totalPages={historyData.pagination?.total_pages || 1}
|
||||
onPageChange={setHistoryPage}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Fallback: Old Run History (if enhanced data not available) */}
|
||||
{!historyData && activeSite && <RunHistory siteId={activeSite.id} />}
|
||||
{/* Meaningful Run History - Full Width */}
|
||||
<MeaningfulRunHistory
|
||||
runs={productionStats?.meaningful_runs || []}
|
||||
loading={loading}
|
||||
maxRuns={10}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Automation Run Detail Page
|
||||
* Comprehensive view of a single automation run
|
||||
*/
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useSiteStore } from '../../store/siteStore';
|
||||
import { automationService } from '../../services/automationService';
|
||||
@@ -20,29 +20,74 @@ const AutomationRunDetail: React.FC = () => {
|
||||
const { runId } = useParams<{ runId: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { activeSite } = useSiteStore();
|
||||
const toast = useToast();
|
||||
const { error: toastError } = useToast();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [runDetail, setRunDetail] = useState<RunDetailResponse | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const lastRequestKey = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadRunDetail();
|
||||
}, [runId, activeSite]);
|
||||
|
||||
const loadRunDetail = async () => {
|
||||
if (!activeSite || !runId) return;
|
||||
|
||||
const decodeTitle = (value: string | undefined | null) => {
|
||||
if (!value) return '';
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await automationService.getRunDetail(activeSite.id, runId);
|
||||
setRunDetail(data);
|
||||
} catch (error: any) {
|
||||
console.error('Failed to load run detail', error);
|
||||
toast.error(error.message || 'Failed to load run detail');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
return decodeURIComponent(value);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
const getDisplayTitle = () => {
|
||||
const run = runDetail?.run;
|
||||
if (!run) return 'Automation Run';
|
||||
if (run.site_name) return run.site_name;
|
||||
if (run.site_domain) return run.site_domain.replace('www.', '');
|
||||
|
||||
const decoded = decodeTitle(run.run_title);
|
||||
if (decoded) return decoded;
|
||||
if (run.run_number) return `Run #${run.run_number}`;
|
||||
return 'Automation Run';
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const loadRunDetail = async () => {
|
||||
if (!runId) {
|
||||
setError('Missing run id');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!activeSite) {
|
||||
setError('Please select a site to view automation run details.');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const requestKey = `${activeSite.id}-${runId}`;
|
||||
if (lastRequestKey.current === requestKey) {
|
||||
return;
|
||||
}
|
||||
lastRequestKey.current = requestKey;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const data = await automationService.getRunDetail(activeSite.id, runId);
|
||||
setRunDetail(data);
|
||||
} catch (err: any) {
|
||||
console.error('Failed to load run detail', err);
|
||||
const message = err?.message === 'Internal server error'
|
||||
? 'Run detail is temporarily unavailable (server error). Please try again later.'
|
||||
: err?.message || 'Failed to load run detail';
|
||||
setError(message);
|
||||
toastError(message);
|
||||
lastRequestKey.current = null;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadRunDetail();
|
||||
}, [runId, activeSite, toastError]);
|
||||
|
||||
if (!activeSite) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
@@ -59,6 +104,14 @@ const AutomationRunDetail: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<p className="text-gray-600 dark:text-gray-400">{error}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!runDetail) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
@@ -67,26 +120,71 @@ const AutomationRunDetail: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const displayTitle = getDisplayTitle();
|
||||
const breadcrumbLabel = runDetail.run?.run_number ? `Run #${runDetail.run.run_number}` : displayTitle;
|
||||
const normalizedRun = runDetail.run ? { ...runDetail.run, run_title: displayTitle } : null;
|
||||
const stageSummary = (runDetail.stages || []).reduce(
|
||||
(acc, stage) => {
|
||||
acc.itemsProcessed += stage.items_processed || 0;
|
||||
acc.itemsCreated += stage.items_created || 0;
|
||||
if (stage.stage_number === 4) acc.contentCreated += stage.items_created || 0;
|
||||
if (stage.stage_number === 6) acc.imagesGenerated += stage.items_created || 0;
|
||||
return acc;
|
||||
},
|
||||
{ itemsProcessed: 0, itemsCreated: 0, contentCreated: 0, imagesGenerated: 0 }
|
||||
);
|
||||
|
||||
const derivedInsights = [] as RunDetailResponse['insights'];
|
||||
if (normalizedRun) {
|
||||
if ((normalizedRun.total_credits_used || 0) > 0 && stageSummary.itemsCreated === 0) {
|
||||
derivedInsights.push({
|
||||
type: 'warning',
|
||||
severity: 'warning',
|
||||
message: 'Credits were spent but no outputs were recorded. Review stage errors and retry failed steps.',
|
||||
});
|
||||
}
|
||||
if (normalizedRun.status === 'running') {
|
||||
derivedInsights.push({
|
||||
type: 'success',
|
||||
severity: 'info',
|
||||
message: `Run is currently active in stage ${normalizedRun.current_stage || 1}.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if ((runDetail.stages || []).some(stage => stage.status === 'failed')) {
|
||||
const failedStage = runDetail.stages.find(stage => stage.status === 'failed');
|
||||
derivedInsights.push({
|
||||
type: 'error',
|
||||
severity: 'error',
|
||||
message: `Stage ${failedStage?.stage_number} failed. Review the stage details and error message for remediation.`,
|
||||
});
|
||||
}
|
||||
|
||||
const combinedInsights = runDetail.insights && runDetail.insights.length > 0
|
||||
? [...runDetail.insights, ...derivedInsights]
|
||||
: derivedInsights;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta
|
||||
title={`Run Detail - ${runDetail.run?.run_title || 'Automation Run'}`}
|
||||
title={`Run Detail - ${displayTitle}`}
|
||||
description="Detailed automation run analysis"
|
||||
/>
|
||||
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title={runDetail.run?.run_title || 'Automation Run'}
|
||||
breadcrumb={`Automation / Runs / ${runDetail.run?.run_title || 'Detail'}`}
|
||||
title={displayTitle}
|
||||
breadcrumb={`Automation / Runs / ${breadcrumbLabel || 'Detail'}`}
|
||||
description="Comprehensive run analysis with stage breakdown and performance metrics"
|
||||
/>
|
||||
|
||||
{/* Run Summary */}
|
||||
{runDetail.run && <RunSummaryCard run={runDetail.run} />}
|
||||
{normalizedRun && <RunSummaryCard run={normalizedRun} summary={stageSummary} />}
|
||||
|
||||
{/* Insights Panel */}
|
||||
{runDetail.insights && runDetail.insights.length > 0 && (
|
||||
<InsightsPanel insights={runDetail.insights} />
|
||||
{combinedInsights.length > 0 && (
|
||||
<InsightsPanel insights={combinedInsights} />
|
||||
)}
|
||||
|
||||
{/* Two Column Layout */}
|
||||
|
||||
Reference in New Issue
Block a user