automation overview page implemeantion initital complete

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-17 08:24:44 +00:00
parent 79398c908d
commit 6b1fa0c1ee
22 changed files with 3789 additions and 178 deletions

View File

@@ -49,6 +49,7 @@ const Approved = lazy(() => import("./pages/Writer/Approved"));
// Automation Module - Lazy loaded
const AutomationPage = lazy(() => import("./pages/Automation/AutomationPage"));
const AutomationOverview = lazy(() => import("./pages/Automation/AutomationOverview"));
const AutomationRunDetail = lazy(() => import("./pages/Automation/AutomationRunDetail"));
const PipelineSettings = lazy(() => import("./pages/Automation/PipelineSettings"));
// Linker Module - Lazy loaded
@@ -198,6 +199,7 @@ export default function App() {
{/* Automation Module */}
<Route path="/automation" element={<Navigate to="/automation/overview" replace />} />
<Route path="/automation/overview" element={<AutomationOverview />} />
<Route path="/automation/runs/:runId" element={<AutomationRunDetail />} />
<Route path="/automation/settings" element={<PipelineSettings />} />
<Route path="/automation/run" element={<AutomationPage />} />

View File

@@ -0,0 +1,49 @@
/**
* Attention Items Alert Component
* Shows items that need attention (failures, skipped items)
*/
import React from 'react';
import { AttentionItems } from '../../../types/automation';
import { ExclamationTriangleIcon } from '../../../icons';
interface AttentionItemsAlertProps {
items: AttentionItems;
}
const AttentionItemsAlert: React.FC<AttentionItemsAlertProps> = ({ items }) => {
if (!items) return null;
const totalIssues = (items.skipped_ideas || 0) + (items.failed_content || 0) + (items.failed_images || 0);
if (totalIssues === 0) {
return null;
}
return (
<div className="bg-warning-50 dark:bg-warning-900/20 border border-warning-200 dark:border-warning-800 rounded-xl p-4">
<div className="flex items-start gap-3">
<div className="flex-shrink-0">
<ExclamationTriangleIcon className="size-6 text-warning-600 dark:text-warning-400" />
</div>
<div className="flex-1">
<h4 className="text-sm font-semibold text-warning-900 dark:text-warning-200 mb-2">
Items Requiring Attention
</h4>
<div className="space-y-1 text-sm text-warning-800 dark:text-warning-300">
{items.skipped_ideas > 0 && (
<div> {items.skipped_ideas} content idea{items.skipped_ideas > 1 ? 's' : ''} skipped</div>
)}
{items.failed_content > 0 && (
<div> {items.failed_content} content piece{items.failed_content > 1 ? 's' : ''} failed generation</div>
)}
{items.failed_images > 0 && (
<div> {items.failed_images} image{items.failed_images > 1 ? 's' : ''} failed generation</div>
)}
</div>
</div>
</div>
</div>
);
};
export default AttentionItemsAlert;

View File

@@ -0,0 +1,102 @@
/**
* Credit Breakdown Chart Component
* Donut chart showing credit distribution across stages
*/
import React from 'react';
import { DetailedStage } from '../../../types/automation';
import ReactApexChart from 'react-apexcharts';
import { ApexOptions } from 'apexcharts';
interface CreditBreakdownChartProps {
stages: DetailedStage[];
}
const CreditBreakdownChart: React.FC<CreditBreakdownChartProps> = ({ stages }) => {
// Filter stages with credits used
const stagesWithCredits = (stages || []).filter(s => (s.credits_used || 0) > 0);
if (stagesWithCredits.length === 0) {
return (
<div className="bg-white dark:bg-gray-900 rounded-xl p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Credit Distribution
</h3>
<div className="text-center py-8 text-gray-600 dark:text-gray-400">
No credits used
</div>
</div>
);
}
const chartData = stagesWithCredits.map(s => s.credits_used);
const chartLabels = stagesWithCredits.map(s => `Stage ${s.stage_number}`);
const chartOptions: ApexOptions = {
chart: {
type: 'donut',
fontFamily: 'Inter, sans-serif',
},
labels: chartLabels,
colors: ['#3b82f6', '#8b5cf6', '#f59e0b', '#10b981', '#06b6d4', '#ec4899', '#6366f1'],
legend: {
position: 'bottom',
labels: {
colors: '#9ca3af',
},
},
plotOptions: {
pie: {
donut: {
size: '70%',
labels: {
show: true,
name: {
show: true,
fontSize: '12px',
color: '#9ca3af',
},
value: {
show: true,
fontSize: '20px',
fontWeight: 600,
color: '#111827',
formatter: (val: string) => `${parseFloat(val).toFixed(0)}`,
},
total: {
show: true,
label: 'Total Credits',
fontSize: '12px',
color: '#9ca3af',
formatter: () => `${chartData.reduce((a, b) => a + b, 0)}`,
},
},
},
},
},
dataLabels: {
enabled: false,
},
tooltip: {
theme: 'dark',
y: {
formatter: (val: number) => `${val} credits`,
},
},
};
return (
<div className="bg-white dark:bg-gray-900 rounded-xl p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Credit Distribution
</h3>
<ReactApexChart
options={chartOptions}
series={chartData}
type="donut"
height={280}
/>
</div>
);
};
export default CreditBreakdownChart;

View File

@@ -0,0 +1,82 @@
/**
* Efficiency Metrics Component
* Displays efficiency statistics and historical comparison
*/
import React from 'react';
import { EfficiencyMetrics as EfficiencyMetricsType, HistoricalComparison } from '../../../types/automation';
interface EfficiencyMetricsProps {
efficiency: EfficiencyMetricsType;
historicalComparison: HistoricalComparison;
}
const EfficiencyMetrics: React.FC<EfficiencyMetricsProps> = ({ efficiency, historicalComparison }) => {
// Add null safety
if (!efficiency || !historicalComparison) {
return (
<div className="bg-white dark:bg-gray-900 rounded-xl p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Efficiency Metrics
</h3>
<div className="text-center py-8 text-gray-600 dark:text-gray-400">
Loading metrics...
</div>
</div>
);
}
const getVarianceColor = (current: number, historical: number) => {
if (historical === 0) return 'text-gray-600 dark:text-gray-400';
const variance = ((current - historical) / historical) * 100;
if (Math.abs(variance) < 10) return 'text-gray-600 dark:text-gray-400';
if (variance > 0) return 'text-error-600 dark:text-error-400';
return 'text-success-600 dark:text-success-400';
};
const getVarianceText = (current: number, historical: number) => {
if (historical === 0) return '';
const variance = ((current - historical) / historical) * 100;
return `${variance > 0 ? '+' : ''}${variance.toFixed(1)}%`;
};
return (
<div className="bg-white dark:bg-gray-900 rounded-xl p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Efficiency Metrics
</h3>
<div className="space-y-4">
<div>
<div className="flex items-center justify-between mb-1">
<span className="text-sm text-gray-600 dark:text-gray-400">Credits per Item</span>
<span className={`text-xs font-medium ${getVarianceColor(efficiency.credits_per_item || 0, historicalComparison.avg_credits_per_item || 0)}`}>
{getVarianceText(efficiency.credits_per_item || 0, historicalComparison.avg_credits_per_item || 0)}
</span>
</div>
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{(efficiency.credits_per_item || 0).toFixed(2)}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Avg: {(historicalComparison.avg_credits_per_item || 0).toFixed(2)}
</div>
</div>
<div className="border-t border-gray-200 dark:border-gray-700 pt-4">
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Items per Minute</div>
<div className="text-xl font-semibold text-gray-900 dark:text-white">
{(efficiency.items_per_minute || 0).toFixed(2)}
</div>
</div>
<div className="border-t border-gray-200 dark:border-gray-700 pt-4">
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Credits per Minute</div>
<div className="text-xl font-semibold text-gray-900 dark:text-white">
{(efficiency.credits_per_minute || 0).toFixed(2)}
</div>
</div>
</div>
</div>
);
};
export default EfficiencyMetrics;

View File

@@ -0,0 +1,222 @@
/**
* Enhanced Run History Component
* Displays automation run history with enhanced data and clickable rows
*/
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { EnhancedRunHistoryItem, StageStatus } from '../../../types/automation';
import { formatDistanceToNow } from '../../../utils/dateUtils';
interface EnhancedRunHistoryProps {
runs: EnhancedRunHistoryItem[];
loading?: boolean;
onPageChange?: (page: number) => void;
currentPage?: number;
totalPages?: number;
}
const EnhancedRunHistory: React.FC<EnhancedRunHistoryProps> = ({
runs,
loading,
onPageChange,
currentPage = 1,
totalPages = 1,
}) => {
const navigate = useNavigate();
const getStatusBadge = (status: string) => {
const colors: Record<string, string> = {
completed: 'bg-success-100 text-success-800 dark:bg-success-900/30 dark:text-success-400',
running: 'bg-brand-100 text-brand-800 dark:bg-brand-900/30 dark:text-brand-400',
paused: 'bg-warning-100 text-warning-800 dark:bg-warning-900/30 dark:text-warning-400',
failed: 'bg-error-100 text-error-800 dark:bg-error-900/30 dark:text-error-400',
cancelled: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300',
};
return colors[status] || 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300';
};
const getStageStatusIcon = (status: StageStatus) => {
switch (status) {
case 'completed':
return '✓';
case 'failed':
return '✗';
case 'skipped':
return '○';
case 'pending':
return '·';
default:
return '·';
}
};
const getStageStatusColor = (status: StageStatus) => {
switch (status) {
case 'completed':
return 'text-success-600 dark:text-success-400';
case 'failed':
return 'text-error-600 dark:text-error-400';
case 'skipped':
return 'text-gray-400 dark:text-gray-600';
case 'pending':
return 'text-gray-300 dark:text-gray-700';
default:
return 'text-gray-300 dark:text-gray-700';
}
};
const formatDuration = (seconds: number): string => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) return `${hours}h ${minutes}m`;
if (minutes > 0) return `${minutes}m`;
return `${seconds}s`;
};
if (loading) {
return (
<div className="bg-white dark:bg-gray-900 rounded-xl p-6">
<div className="animate-pulse space-y-4">
{[1, 2, 3].map(i => (
<div key={i} className="h-20 bg-gray-200 dark:bg-gray-700 rounded"></div>
))}
</div>
</div>
);
}
if (runs.length === 0) {
return (
<div className="bg-white dark:bg-gray-900 rounded-xl p-8 text-center">
<p className="text-gray-600 dark:text-gray-400">No automation runs yet</p>
</div>
);
}
return (
<div className="bg-white dark:bg-gray-900 rounded-xl overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-800/50">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Run
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Status
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Stages
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Duration
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Results
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Credits
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Started
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{runs.map((run) => (
<tr
key={run.run_id}
onClick={() => navigate(`/automation/runs/${run.run_id}`)}
className="hover:bg-gray-50 dark:hover:bg-gray-800/30 cursor-pointer transition-colors"
>
<td className="px-4 py-4">
<div className="text-sm font-medium text-brand-600 dark:text-brand-400 hover:underline">
{run.run_title}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400 capitalize">
{run.trigger_type}
</div>
</td>
<td className="px-4 py-4">
<span
className={`inline-flex px-2 py-1 rounded-full text-xs font-semibold ${getStatusBadge(
run.status
)}`}
>
{run.status}
</span>
</td>
<td className="px-4 py-4">
<div className="flex items-center gap-1">
{(run.stage_statuses || []).map((status, idx) => (
<span
key={idx}
className={`text-lg font-bold ${getStageStatusColor(status)}`}
title={`Stage ${idx + 1}: ${status}`}
>
{getStageStatusIcon(status)}
</span>
))}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{run.stages_completed || 0}/7 completed
</div>
</td>
<td className="px-4 py-4">
<div className="text-sm text-gray-900 dark:text-white">
{formatDuration(run.duration_seconds || 0)}
</div>
</td>
<td className="px-4 py-4">
<div className="text-sm text-gray-900 dark:text-white">
{run.summary?.items_processed || 0} {run.summary?.items_created || 0}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400">
{run.summary?.content_created || 0} content, {run.summary?.images_generated || 0} images
</div>
</td>
<td className="px-4 py-4">
<div className="text-sm font-medium text-gray-900 dark:text-white">
{(run.total_credits_used || 0).toLocaleString()}
</div>
</td>
<td className="px-4 py-4">
<div className="text-sm text-gray-900 dark:text-white">
{formatDistanceToNow(run.started_at)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Pagination */}
{totalPages > 1 && onPageChange && (
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between">
<button
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage === 1}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Previous
</button>
<span className="text-sm text-gray-600 dark:text-gray-400">
Page {currentPage} of {totalPages}
</span>
<button
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage === totalPages}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
</button>
</div>
</div>
)}
</div>
);
};
export default EnhancedRunHistory;

View File

@@ -0,0 +1,66 @@
/**
* Insights Panel Component
* Displays auto-generated insights and alerts
*/
import React from 'react';
import { RunInsight } from '../../../types/automation';
import { CheckCircleIcon, ExclamationTriangleIcon, InfoIcon, XCircleIcon } from '../../../icons';
interface InsightsPanelProps {
insights: RunInsight[];
}
const InsightsPanel: React.FC<InsightsPanelProps> = ({ insights }) => {
if (!insights || insights.length === 0) {
return null;
}
const getInsightStyle = (severity: string) => {
switch (severity) {
case 'error':
return 'bg-error-50 dark:bg-error-900/20 border-error-200 dark:border-error-800';
case 'warning':
return 'bg-warning-50 dark:bg-warning-900/20 border-warning-200 dark:border-warning-800';
case 'info':
default:
return 'bg-brand-50 dark:bg-brand-900/20 border-brand-200 dark:border-brand-800';
}
};
const getInsightIcon = (type: string) => {
switch (type) {
case 'success':
return <CheckCircleIcon className="size-5 text-success-600 dark:text-success-400" />;
case 'error':
return <XCircleIcon className="size-5 text-error-600 dark:text-error-400" />;
case 'warning':
case 'variance':
return <ExclamationTriangleIcon className="size-5 text-warning-600 dark:text-warning-400" />;
default:
return <InfoIcon className="size-5 text-brand-600 dark:text-brand-400" />;
}
};
return (
<div className="bg-white dark:bg-gray-900 rounded-xl p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">Insights</h3>
<div className="space-y-3">
{insights.map((insight, idx) => (
<div
key={idx}
className={`flex items-start gap-3 p-4 rounded-lg border ${getInsightStyle(insight.severity)}`}
>
<div className="flex-shrink-0 mt-0.5">
{getInsightIcon(insight.type)}
</div>
<div className="flex-1 text-sm text-gray-800 dark:text-gray-200">
{insight.message}
</div>
</div>
))}
</div>
</div>
);
};
export default InsightsPanel;

View File

@@ -0,0 +1,182 @@
/**
* Predictive Cost Analysis Component
* Shows estimated credits and outputs for next automation run
*/
import React from 'react';
import { PredictiveAnalysis } from '../../../types/automation';
import ReactApexChart from 'react-apexcharts';
import { ApexOptions } from 'apexcharts';
interface PredictiveCostAnalysisProps {
analysis: PredictiveAnalysis;
loading?: boolean;
}
const PredictiveCostAnalysis: React.FC<PredictiveCostAnalysisProps> = ({ analysis, loading }) => {
if (loading || !analysis) {
return (
<div className="bg-white dark:bg-gray-900 rounded-xl p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Predictive Cost Analysis
</h3>
<div className="animate-pulse">
<div className="h-64 bg-gray-200 dark:bg-gray-700 rounded"></div>
</div>
</div>
);
}
const confidenceColors = {
high: 'text-success-600 dark:text-success-400',
medium: 'text-warning-600 dark:text-warning-400',
low: 'text-error-600 dark:text-error-400',
};
const confidenceBadges = {
high: 'bg-success-100 text-success-800 dark:bg-success-900/30 dark:text-success-400',
medium: 'bg-warning-100 text-warning-800 dark:bg-warning-900/30 dark:text-warning-400',
low: 'bg-error-100 text-error-800 dark:bg-error-900/30 dark:text-error-400',
};
// Prepare data for donut chart
const chartData = (analysis.stages || [])
.filter(s => (s.estimated_credits || 0) > 0)
.map(s => s.estimated_credits || 0);
const chartLabels = (analysis.stages || [])
.filter(s => (s.estimated_credits || 0) > 0)
.map(s => s.stage_name || 'Unknown');
const chartOptions: ApexOptions = {
chart: {
type: 'donut',
fontFamily: 'Inter, sans-serif',
},
labels: chartLabels,
colors: ['#3b82f6', '#8b5cf6', '#f59e0b', '#10b981', '#06b6d4', '#ec4899', '#6366f1'],
legend: {
position: 'bottom',
labels: {
colors: '#9ca3af',
},
},
plotOptions: {
pie: {
donut: {
size: '70%',
labels: {
show: true,
name: {
show: true,
fontSize: '14px',
color: '#9ca3af',
},
value: {
show: true,
fontSize: '24px',
fontWeight: 600,
color: '#111827',
formatter: (val: string) => `${parseFloat(val).toFixed(0)} cr`,
},
total: {
show: true,
label: 'Est. Total',
fontSize: '14px',
color: '#9ca3af',
formatter: () => `${analysis.totals?.total_estimated_credits || 0} cr`,
},
},
},
},
},
dataLabels: {
enabled: false,
},
tooltip: {
theme: 'dark',
y: {
formatter: (val: number) => `${val} credits`,
},
},
};
return (
<div className="bg-white dark:bg-gray-900 rounded-xl p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
Predictive Cost Analysis
</h3>
<span className={`px-3 py-1 rounded-full text-xs font-semibold ${confidenceBadges[analysis.confidence || 'medium']}`}>
{(analysis.confidence || 'medium').toUpperCase()} confidence
</span>
</div>
{/* Summary Cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div className="text-center">
<div className="text-2xl font-bold text-brand-600 dark:text-brand-400">
{analysis.totals?.total_pending_items || 0}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">Pending Items</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-success-600 dark:text-success-400">
{analysis.totals?.total_estimated_output || 0}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">Est. Output</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-warning-600 dark:text-warning-400">
{analysis.totals?.total_estimated_credits || 0}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">Est. Credits</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{analysis.totals?.recommended_buffer_credits || 0}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">+20% Buffer</div>
</div>
</div>
{/* Donut Chart */}
{chartData.length > 0 && (
<div className="mb-6">
<ReactApexChart
options={chartOptions}
series={chartData}
type="donut"
height={300}
/>
</div>
)}
{/* Stage Breakdown */}
<div className="space-y-2">
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">Stage Breakdown</h4>
{(analysis.stages || []).map((stage) => (
<div
key={stage.stage_number || 0}
className="flex items-center justify-between py-2 border-b border-gray-100 dark:border-gray-800 last:border-0"
>
<div className="flex-1">
<div className="text-sm font-medium text-gray-900 dark:text-white">
{stage.stage_name || 'Unknown Stage'}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400">
{stage.pending_items || 0} items ~{stage.estimated_output || 0} output
</div>
</div>
<div className="text-right">
<div className="text-sm font-semibold text-gray-900 dark:text-white">
~{stage.estimated_credits || 0} cr
</div>
</div>
</div>
))}
</div>
</div>
);
};
export default PredictiveCostAnalysis;

View File

@@ -0,0 +1,120 @@
/**
* Run Statistics Summary Component
* Displays aggregate statistics about automation runs
*/
import React from 'react';
import { RunStatistics } from '../../../types/automation';
import { BoltIcon, CheckCircleIcon, XCircleIcon, ClockIcon } from '../../../icons';
interface RunStatisticsSummaryProps {
statistics: RunStatistics;
loading?: boolean;
}
const RunStatisticsSummary: React.FC<RunStatisticsSummaryProps> = ({ statistics, loading }) => {
if (loading || !statistics) {
return (
<div className="bg-white dark:bg-gray-900 rounded-xl p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">Run Statistics</h3>
<div className="animate-pulse space-y-4">
<div className="h-20 bg-gray-200 dark:bg-gray-700 rounded"></div>
<div className="h-20 bg-gray-200 dark:bg-gray-700 rounded"></div>
</div>
</div>
);
}
const formatDuration = (seconds: number): string => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) return `${hours}h ${minutes}m`;
if (minutes > 0) return `${minutes}m`;
return `${seconds}s`;
};
const stats = [
{
label: 'Total Runs',
value: statistics.total_runs || 0,
icon: BoltIcon,
color: 'brand' as const,
},
{
label: 'Completed',
value: statistics.completed_runs || 0,
icon: CheckCircleIcon,
color: 'success' as const,
},
{
label: 'Failed',
value: statistics.failed_runs || 0,
icon: XCircleIcon,
color: 'error' as const,
},
{
label: 'Running',
value: statistics.running_runs || 0,
icon: ClockIcon,
color: 'warning' as const,
},
];
return (
<div className="bg-white dark:bg-gray-900 rounded-xl p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">Run Statistics</h3>
{/* Stats Grid */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
{stats.map((stat) => {
const Icon = stat.icon;
const colorClasses = {
brand: 'bg-brand-100 dark:bg-brand-900/30 text-brand-600 dark:text-brand-400',
success: 'bg-success-100 dark:bg-success-900/30 text-success-600 dark:text-success-400',
error: 'bg-error-100 dark:bg-error-900/30 text-error-600 dark:text-error-400',
warning: 'bg-warning-100 dark:bg-warning-900/30 text-warning-600 dark:text-warning-400',
};
return (
<div key={stat.label} className="text-center">
<div className={`inline-flex size-12 rounded-lg items-center justify-center mb-2 ${colorClasses[stat.color]}`}>
<Icon className="size-6" />
</div>
<div className="text-2xl font-bold text-gray-900 dark:text-white">{stat.value}</div>
<div className="text-sm text-gray-500 dark:text-gray-400">{stat.label}</div>
</div>
);
})}
</div>
{/* Additional Metrics */}
<div className="border-t border-gray-200 dark:border-gray-700 pt-4 space-y-3">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600 dark:text-gray-400">Total Credits Used</span>
<span className="text-lg font-semibold text-gray-900 dark:text-white">
{(statistics.total_credits_used || 0).toLocaleString()}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600 dark:text-gray-400">Last 30 Days</span>
<span className="text-base font-medium text-brand-600 dark:text-brand-400">
{(statistics.total_credits_last_30_days || 0).toLocaleString()} credits
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600 dark:text-gray-400">Avg Credits/Run</span>
<span className="text-base font-medium text-gray-900 dark:text-white">
{Math.round(statistics.avg_credits_per_run || 0).toLocaleString()}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600 dark:text-gray-400">Avg Duration (7 days)</span>
<span className="text-base font-medium text-gray-900 dark:text-white">
{formatDuration(statistics.avg_duration_last_7_days_seconds || 0)}
</span>
</div>
</div>
</div>
);
};
export default RunStatisticsSummary;

View File

@@ -0,0 +1,97 @@
/**
* Run Summary Card Component
* Displays header information about an automation run
*/
import React from 'react';
import { RunDetailInfo } from '../../../types/automation';
import { formatDateTime, formatDuration } from '../../../utils/dateUtils';
import { CheckCircleIcon, XCircleIcon, ClockIcon, BoltIcon } from '../../../icons';
interface RunSummaryCardProps {
run: RunDetailInfo;
}
const RunSummaryCard: React.FC<RunSummaryCardProps> = ({ run }) => {
if (!run) {
return (
<div className="bg-white dark:bg-gray-900 rounded-xl p-6">
<div className="animate-pulse h-20 bg-gray-200 dark:bg-gray-700 rounded"></div>
</div>
);
}
const getStatusIcon = () => {
switch (run.status) {
case 'completed':
return <CheckCircleIcon className="size-6 text-success-600 dark:text-success-400" />;
case 'failed':
return <XCircleIcon className="size-6 text-error-600 dark:text-error-400" />;
case 'running':
return <ClockIcon className="size-6 text-brand-600 dark:text-brand-400" />;
default:
return <BoltIcon className="size-6 text-gray-600 dark:text-gray-400" />;
}
};
const getStatusBadge = () => {
const colors: Record<string, string> = {
completed: 'bg-success-100 text-success-800 dark:bg-success-900/30 dark:text-success-400',
running: 'bg-brand-100 text-brand-800 dark:bg-brand-900/30 dark:text-brand-400',
paused: 'bg-warning-100 text-warning-800 dark:bg-warning-900/30 dark:text-warning-400',
failed: 'bg-error-100 text-error-800 dark:bg-error-900/30 dark:text-error-400',
cancelled: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300',
};
return colors[run.status] || 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300';
};
const totalInitialItems = run.initial_snapshot?.total_initial_items || 0;
return (
<div className="bg-white dark:bg-gray-900 rounded-xl p-6">
<div className="flex items-start gap-4">
<div className="flex-shrink-0">
{getStatusIcon()}
</div>
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<span className={`px-3 py-1 rounded-full text-xs font-semibold ${getStatusBadge()}`}>
{(run.status || 'unknown').toUpperCase()}
</span>
<span className="text-sm text-gray-600 dark:text-gray-400 capitalize">
{run.trigger_type || 'manual'} trigger
</span>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 mt-4">
<div>
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">Started</div>
<div className="text-sm font-medium text-gray-900 dark:text-white">
{formatDateTime(run.started_at)}
</div>
</div>
<div>
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">Duration</div>
<div className="text-sm font-medium text-gray-900 dark:text-white">
{formatDuration(run.duration_seconds || 0)}
</div>
</div>
<div>
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">Items Processed</div>
<div className="text-sm font-medium text-gray-900 dark:text-white">
{totalInitialItems}
</div>
</div>
<div>
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">Total Credits</div>
<div className="text-sm font-medium text-brand-600 dark:text-brand-400">
{(run.total_credits_used || 0).toLocaleString()}
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default RunSummaryCard;

View File

@@ -0,0 +1,190 @@
/**
* Stage Accordion Component
* Expandable sections showing detailed stage information
*/
import React, { useState } from 'react';
import { DetailedStage, InitialSnapshot } from '../../../types/automation';
import { formatDuration } from '../../../utils/dateUtils';
import { ChevronDownIcon, ChevronUpIcon, CheckCircleIcon, XCircleIcon, AlertCircleIcon } from '../../../icons';
interface StageAccordionProps {
stages: DetailedStage[];
initialSnapshot: InitialSnapshot;
}
const StageAccordion: React.FC<StageAccordionProps> = ({ stages, initialSnapshot }) => {
const [expandedStages, setExpandedStages] = useState<Set<number>>(new Set([1]));
if (!stages || stages.length === 0) {
return (
<div className="bg-white dark:bg-gray-900 rounded-xl p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Stage Details</h3>
<div className="text-center py-8 text-gray-600 dark:text-gray-400">No stage data available</div>
</div>
);
}
const toggleStage = (stageNumber: number) => {
const newExpanded = new Set(expandedStages);
if (newExpanded.has(stageNumber)) {
newExpanded.delete(stageNumber);
} else {
newExpanded.add(stageNumber);
}
setExpandedStages(newExpanded);
};
const getStageIcon = (status: string) => {
switch (status) {
case 'completed':
return <CheckCircleIcon className="size-5 text-success-600 dark:text-success-400" />;
case 'failed':
return <XCircleIcon className="size-5 text-error-600 dark:text-error-400" />;
case 'skipped':
return <AlertCircleIcon className="size-5 text-gray-400 dark:text-gray-600" />;
default:
return <div className="size-5 rounded-full border-2 border-gray-300 dark:border-gray-600" />;
}
};
const getVarianceColor = (variance: number) => {
if (Math.abs(variance) < 10) return 'text-gray-600 dark:text-gray-400';
if (variance > 0) return 'text-error-600 dark:text-error-400';
return 'text-success-600 dark:text-success-400';
};
return (
<div className="bg-white dark:bg-gray-900 rounded-xl overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Stage Details</h3>
</div>
<div className="divide-y divide-gray-200 dark:divide-gray-700">
{stages.map((stage) => {
const isExpanded = expandedStages.has(stage.stage_number);
return (
<div key={stage.stage_number}>
{/* Stage Header */}
<button
onClick={() => toggleStage(stage.stage_number)}
className="w-full px-6 py-4 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors"
>
<div className="flex items-center gap-4 flex-1">
<div className="flex-shrink-0">
{getStageIcon(stage.status)}
</div>
<div className="text-left flex-1">
<div className="font-medium text-gray-900 dark:text-white">
Stage {stage.stage_number}: {stage.stage_name}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{stage.items_processed || 0} {stage.items_created || 0} items {stage.credits_used || 0} credits
{(stage.duration_seconds || 0) > 0 && `${formatDuration(stage.duration_seconds || 0)}`}
</div>
</div>
<div className="text-right">
<div className="text-sm font-semibold text-gray-900 dark:text-white">
{stage.credits_used || 0} cr
</div>
</div>
<div className="flex-shrink-0">
{isExpanded ? (
<ChevronUpIcon className="size-5 text-gray-400" />
) : (
<ChevronDownIcon className="size-5 text-gray-400" />
)}
</div>
</div>
</button>
{/* Stage Details */}
{isExpanded && (
<div className="px-6 py-4 bg-gray-50 dark:bg-gray-800/30 space-y-4">
{/* Metrics Grid */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">Input</div>
<div className="text-lg font-semibold text-gray-900 dark:text-white">
{stage.items_processed || 0}
</div>
</div>
<div>
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">Output</div>
<div className="text-lg font-semibold text-gray-900 dark:text-white">
{stage.items_created || 0}
</div>
</div>
<div>
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">Credits</div>
<div className="text-lg font-semibold text-brand-600 dark:text-brand-400">
{stage.credits_used || 0}
</div>
</div>
<div>
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">Duration</div>
<div className="text-lg font-semibold text-gray-900 dark:text-white">
{formatDuration(stage.duration_seconds || 0)}
</div>
</div>
</div>
{/* Historical Comparison */}
{stage.comparison && (stage.comparison.historical_avg_credits || 0) > 0 && (
<div className="border-t border-gray-200 dark:border-gray-700 pt-4">
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
Historical Comparison
</h4>
<div className="grid grid-cols-2 gap-4">
<div>
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">
Credits vs Average
</div>
<div className={`text-sm font-semibold ${getVarianceColor(stage.comparison.credit_variance_pct || 0)}`}>
{(stage.comparison.credit_variance_pct || 0) > 0 ? '+' : ''}
{(stage.comparison.credit_variance_pct || 0).toFixed(1)}%
<span className="text-xs text-gray-500 dark:text-gray-400 ml-1">
(avg: {(stage.comparison.historical_avg_credits || 0).toFixed(0)})
</span>
</div>
</div>
<div>
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">
Output vs Average
</div>
<div className={`text-sm font-semibold ${getVarianceColor(-(stage.comparison.items_variance_pct || 0))}`}>
{(stage.comparison.items_variance_pct || 0) > 0 ? '+' : ''}
{(stage.comparison.items_variance_pct || 0).toFixed(1)}%
<span className="text-xs text-gray-500 dark:text-gray-400 ml-1">
(avg: {(stage.comparison.historical_avg_items || 0).toFixed(0)})
</span>
</div>
</div>
</div>
</div>
)}
{/* Error Message */}
{stage.error && (
<div className="border-t border-gray-200 dark:border-gray-700 pt-4">
<div className="bg-error-50 dark:bg-error-900/20 border border-error-200 dark:border-error-800 rounded-lg p-3">
<div className="text-sm font-semibold text-error-900 dark:text-error-200 mb-1">
Error
</div>
<div className="text-sm text-error-800 dark:text-error-300">
{stage.error}
</div>
</div>
</div>
)}
</div>
)}
</div>
);
})}
</div>
</div>
);
};
export default StageAccordion;

View File

@@ -86,27 +86,27 @@ const RunHistory: React.FC<RunHistoryProps> = ({ siteId }) => {
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{history.map((run) => (
<tr key={run.run_id} className="hover:bg-gray-50 dark:hover:bg-gray-800/30">
<td className="px-4 py-3 text-sm font-mono text-gray-900 dark:text-gray-100">{run.run_id.slice(0, 8)}...</td>
<td className="px-4 py-3 text-sm font-mono text-gray-900 dark:text-gray-100">{(run.run_id || '').slice(0, 8)}...</td>
<td className="px-4 py-3">
<span
className={`px-2 py-1 rounded-full text-xs font-semibold ${getStatusBadge(
run.status
run.status || 'unknown'
)}`}
>
{run.status}
{run.status || 'unknown'}
</span>
</td>
<td className="px-4 py-3 text-sm text-gray-900 dark:text-gray-100 capitalize">{run.trigger_type}</td>
<td className="px-4 py-3 text-sm text-gray-900 dark:text-gray-100 capitalize">{run.trigger_type || 'manual'}</td>
<td className="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
{new Date(run.started_at).toLocaleString()}
{run.started_at ? new Date(run.started_at).toLocaleString() : '-'}
</td>
<td className="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
{run.completed_at
? new Date(run.completed_at).toLocaleString()
: '-'}
</td>
<td className="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">{run.total_credits_used}</td>
<td className="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">{run.current_stage}/7</td>
<td className="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">{run.total_credits_used || 0}</td>
<td className="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">{run.current_stage || 0}/7</td>
</tr>
))}
</tbody>

View File

@@ -137,6 +137,7 @@ export { BoxCubeIcon as SettingsIcon }; // Settings/cog alias
export { InfoIcon as HelpCircleIcon }; // Help/question circle
export { AlertIcon as AlertCircleIcon }; // Alert/warning circle
export { AlertIcon as AlertTriangleIcon }; // Alert triangle alias
export { AlertIcon as ExclamationTriangleIcon }; // Exclamation triangle alias
export { CheckLineIcon as CheckIcon }; // Simple check mark
export { TrashBinIcon as TrashIcon }; // Trash alias
export { TrashBinIcon as Trash2Icon }; // Trash2 alias

View File

@@ -6,6 +6,7 @@ import React, { useState, useEffect } from 'react';
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,
@@ -18,6 +19,10 @@ 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 {
ListIcon,
GroupIcon,
@@ -31,7 +36,9 @@ const AutomationOverview: React.FC = () => {
const toast = useToast();
const [loading, setLoading] = useState(true);
const [metrics, setMetrics] = useState<any>(null);
const [estimate, setEstimate] = useState<any>(null);
const [overviewStats, setOverviewStats] = useState<OverviewStatsResponse | null>(null);
const [historyPage, setHistoryPage] = useState(1);
const [historyData, setHistoryData] = useState<any>(null);
// Load metrics for the 5 metric cards
const loadMetrics = async () => {
@@ -89,28 +96,42 @@ const AutomationOverview: React.FC = () => {
};
// Load cost estimate
const loadEstimate = async () => {
const loadOverviewStats = async () => {
if (!activeSite) return;
try {
const estimateData = await automationService.estimate(activeSite.id);
setEstimate(estimateData);
const stats = await automationService.getOverviewStats(activeSite.id);
setOverviewStats(stats);
} catch (e) {
console.warn('Failed to fetch cost estimate', 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);
}
};
useEffect(() => {
const loadData = async () => {
setLoading(true);
await Promise.all([loadMetrics(), loadEstimate()]);
await Promise.all([loadMetrics(), loadOverviewStats(), loadEnhancedHistory(historyPage)]);
setLoading(false);
};
if (activeSite) {
loadData();
}
}, [activeSite]);
}, [activeSite, historyPage]);
// Helper to render metric rows
const renderMetricRow = (items: Array<{ label: string; value: number; colorCls: string }>) => {
@@ -253,34 +274,50 @@ const AutomationOverview: React.FC = () => {
</div>
{/* Cost Estimation Card */}
{estimate && (
<ComponentCard
title="Ready to Process"
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<div className="space-y-3">
<div className="text-sm font-medium text-gray-700 dark:text-gray-300">
Estimated Items to Process: <span className="text-lg font-bold text-brand-600">{estimate.estimated_credits || 0}</span>
</div>
<div className="text-sm font-medium text-gray-700 dark:text-gray-300">
Current Balance: <span className="text-lg font-bold text-success-600">{estimate.current_balance || 0}</span> credits
</div>
<div className="text-sm font-medium text-gray-700 dark:text-gray-300">
Status: {estimate.sufficient ? (
<span className="text-success-600 font-bold"> Sufficient credits</span>
) : (
<span className="text-danger-600 font-bold"> Insufficient credits</span>
)}
</div>
</div>
</div>
{overviewStats ? (
<>
{/* Attention Items Alert */}
{overviewStats.attention_items && (
<AttentionItemsAlert items={overviewStats.attention_items} />
)}
{/* 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>
</ComponentCard>
</>
) : !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>
)}
{/* Run History */}
{activeSite && <RunHistory siteId={activeSite.id} />}
{/* 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} />}
</div>
</>
);

View File

@@ -0,0 +1,120 @@
/**
* Automation Run Detail Page
* Comprehensive view of a single automation run
*/
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useSiteStore } from '../../store/siteStore';
import { automationService } from '../../services/automationService';
import { RunDetailResponse } from '../../types/automation';
import { useToast } from '../../components/ui/toast/ToastContainer';
import PageMeta from '../../components/common/PageMeta';
import PageHeader from '../../components/common/PageHeader';
import RunSummaryCard from '../../components/Automation/DetailView/RunSummaryCard';
import StageAccordion from '../../components/Automation/DetailView/StageAccordion';
import EfficiencyMetrics from '../../components/Automation/DetailView/EfficiencyMetrics';
import InsightsPanel from '../../components/Automation/DetailView/InsightsPanel';
import CreditBreakdownChart from '../../components/Automation/DetailView/CreditBreakdownChart';
const AutomationRunDetail: React.FC = () => {
const { runId } = useParams<{ runId: string }>();
const navigate = useNavigate();
const { activeSite } = useSiteStore();
const toast = useToast();
const [loading, setLoading] = useState(true);
const [runDetail, setRunDetail] = useState<RunDetailResponse | null>(null);
useEffect(() => {
loadRunDetail();
}, [runId, activeSite]);
const loadRunDetail = async () => {
if (!activeSite || !runId) 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);
}
};
if (!activeSite) {
return (
<div className="p-6">
<p className="text-gray-600 dark:text-gray-400">Please select a site to view automation run details.</p>
</div>
);
}
if (loading) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<div className="text-gray-500 dark:text-gray-400">Loading run details...</div>
</div>
);
}
if (!runDetail) {
return (
<div className="p-6">
<p className="text-gray-600 dark:text-gray-400">Run not found.</p>
</div>
);
}
return (
<>
<PageMeta
title={`Run Detail - ${runDetail.run?.run_title || 'Automation Run'}`}
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'}`}
description="Comprehensive run analysis with stage breakdown and performance metrics"
/>
{/* Run Summary */}
{runDetail.run && <RunSummaryCard run={runDetail.run} />}
{/* Insights Panel */}
{runDetail.insights && runDetail.insights.length > 0 && (
<InsightsPanel insights={runDetail.insights} />
)}
{/* Two Column Layout */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Column - Credit Breakdown & Efficiency */}
<div className="lg:col-span-1 space-y-6">
{runDetail.stages && <CreditBreakdownChart stages={runDetail.stages} />}
{runDetail.efficiency && runDetail.historical_comparison && (
<EfficiencyMetrics
efficiency={runDetail.efficiency}
historicalComparison={runDetail.historical_comparison}
/>
)}
</div>
{/* Right Column - Stage Details */}
<div className="lg:col-span-2">
{runDetail.stages && runDetail.run?.initial_snapshot && (
<StageAccordion
stages={runDetail.stages}
initialSnapshot={runDetail.run.initial_snapshot}
/>
)}
</div>
</div>
</div>
</>
);
};
export default AutomationRunDetail;

View File

@@ -232,6 +232,34 @@ export const automationService = {
return response.runs;
},
/**
* Get enhanced automation run history with pagination
*/
getEnhancedHistory: async (
siteId: number,
page: number = 1,
pageSize: number = 20
): Promise<import('../types/automation').HistoryResponse> => {
return fetchAPI(buildUrl('/history/', { site_id: siteId, page, page_size: pageSize }));
},
/**
* Get overview statistics with predictive analysis
*/
getOverviewStats: async (siteId: number): Promise<import('../types/automation').OverviewStatsResponse> => {
return fetchAPI(buildUrl('/overview_stats/', { site_id: siteId }));
},
/**
* Get detailed information about a specific run
*/
getRunDetail: async (
siteId: number,
runId: string
): Promise<import('../types/automation').RunDetailResponse> => {
return fetchAPI(buildUrl('/run_detail/', { site_id: siteId, run_id: runId }));
},
/**
* Get automation run logs
*/

View File

@@ -0,0 +1,174 @@
/**
* Enhanced Automation Types for Detail View
* Matches backend API responses from overview_stats, history, and run_detail endpoints
*/
// Overview Stats Types
export interface RunStatistics {
total_runs: number;
completed_runs: number;
failed_runs: number;
running_runs: number;
total_credits_used: number;
total_credits_last_30_days: number;
avg_credits_per_run: number;
avg_duration_last_7_days_seconds: number;
}
export interface PredictiveStage {
stage_number: number;
stage_name: string;
pending_items: number;
estimated_credits: number;
estimated_output: number;
}
export interface PredictiveAnalysis {
stages: PredictiveStage[];
totals: {
total_pending_items: number;
total_estimated_credits: number;
total_estimated_output: number;
recommended_buffer_credits: number;
};
confidence: 'high' | 'medium' | 'low';
}
export interface AttentionItems {
skipped_ideas: number;
failed_content: number;
failed_images: number;
}
export interface HistoricalStageAverage {
stage_number: number;
stage_name: string;
avg_credits: number;
avg_items_created: number;
avg_output_ratio: number;
}
export interface HistoricalAverages {
avg_total_credits: number;
avg_duration_seconds: number;
avg_credits_per_item: number;
total_runs_analyzed: number;
has_sufficient_data: boolean;
stages: HistoricalStageAverage[];
}
export interface OverviewStatsResponse {
run_statistics: RunStatistics;
predictive_analysis: PredictiveAnalysis;
attention_items: AttentionItems;
historical_averages: HistoricalAverages;
}
// Enhanced History Types
export interface RunSummary {
items_processed: number;
items_created: number;
content_created: number;
images_generated: number;
}
export interface InitialSnapshot {
stage_1_initial: number;
stage_2_initial: number;
stage_3_initial: number;
stage_4_initial: number;
stage_5_initial: number;
stage_6_initial: number;
stage_7_initial: number;
total_initial_items: number;
}
export type StageStatus = 'completed' | 'pending' | 'skipped' | 'failed';
export interface EnhancedRunHistoryItem {
run_id: string;
run_number: number;
run_title: string;
status: 'completed' | 'running' | 'paused' | 'failed' | 'cancelled';
trigger_type: 'manual' | 'scheduled';
started_at: string;
completed_at: string | null;
duration_seconds: number;
total_credits_used: number;
current_stage: number;
stages_completed: number;
stages_failed: number;
initial_snapshot: InitialSnapshot;
summary: RunSummary;
stage_statuses: StageStatus[];
}
export interface HistoryResponse {
runs: EnhancedRunHistoryItem[];
pagination: {
page: number;
page_size: number;
total_count: number;
total_pages: number;
};
}
// Run Detail Types
export interface StageComparison {
historical_avg_credits: number;
historical_avg_items: number;
credit_variance_pct: number;
items_variance_pct: number;
}
export interface DetailedStage {
stage_number: number;
stage_name: string;
status: StageStatus;
credits_used: number;
items_processed: number;
items_created: number;
duration_seconds: number;
error: string;
comparison: StageComparison;
}
export interface EfficiencyMetrics {
credits_per_item: number;
items_per_minute: number;
credits_per_minute: number;
}
export interface RunInsight {
type: 'success' | 'warning' | 'variance' | 'error';
severity: 'info' | 'warning' | 'error';
message: string;
}
export interface RunDetailInfo {
run_id: string;
run_number: number;
run_title: string;
status: 'completed' | 'running' | 'paused' | 'failed' | 'cancelled';
trigger_type: 'manual' | 'scheduled';
started_at: string;
completed_at: string | null;
duration_seconds: number;
current_stage: number;
total_credits_used: number;
initial_snapshot: InitialSnapshot;
}
export interface HistoricalComparison {
avg_credits: number;
avg_duration_seconds: number;
avg_credits_per_item: number;
}
export interface RunDetailResponse {
run: RunDetailInfo;
stages: DetailedStage[];
efficiency: EfficiencyMetrics;
insights: RunInsight[];
historical_comparison: HistoricalComparison;
}

View File

@@ -0,0 +1,38 @@
/**
* Date utility functions
*/
export const formatDistanceToNow = (dateString: string): string => {
const date = new Date(dateString);
const now = new Date();
const seconds = Math.floor((now.getTime() - date.getTime()) / 1000);
if (seconds < 60) return 'just now';
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
if (seconds < 604800) return `${Math.floor(seconds / 86400)}d ago`;
return date.toLocaleDateString();
};
export const formatDateTime = (dateString: string): string => {
const date = new Date(dateString);
return date.toLocaleString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: true,
});
};
export const formatDuration = (seconds: number): string => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
if (hours > 0) return `${hours}h ${minutes}m ${secs}s`;
if (minutes > 0) return `${minutes}m ${secs}s`;
return `${secs}s`;
};