Files
igny8/frontend/src/pages/Automation/AutomationRunDetail.tsx

219 lines
7.3 KiB
TypeScript

/**
* Automation Run Detail Page
* Comprehensive view of a single automation run
*/
import React, { useEffect, useRef, useState } 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 { 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);
const decodeTitle = (value: string | undefined | null) => {
if (!value) return '';
try {
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">
<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 (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">
<p className="text-gray-600 dark:text-gray-400">Run not found.</p>
</div>
);
}
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 - ${displayTitle}`}
description="Detailed automation run analysis"
/>
<div className="space-y-6">
<PageHeader
title={displayTitle}
breadcrumb={`Automation / Runs / ${breadcrumbLabel || 'Detail'}`}
description="Comprehensive run analysis with stage breakdown and performance metrics"
/>
{/* Run Summary */}
{normalizedRun && <RunSummaryCard run={normalizedRun} summary={stageSummary} />}
{/* Insights Panel */}
{combinedInsights.length > 0 && (
<InsightsPanel insights={combinedInsights} />
)}
{/* 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;