219 lines
7.3 KiB
TypeScript
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;
|