This commit is contained in:
IGNY8 VPS (Salman)
2025-12-04 22:43:25 +00:00
parent 1521f3ff8c
commit 8b895dbdc7
18 changed files with 1569 additions and 172 deletions

View File

@@ -3,7 +3,15 @@
* Shows real-time automation progress with pause/resume/cancel controls
*/
import React, { useEffect, useState } from 'react';
import { automationService, ProcessingState, AutomationRun } from '../../services/automationService';
import { automationService, ProcessingState, AutomationRun, PipelineStage } from '../../services/automationService';
import {
fetchKeywords,
fetchClusters,
fetchContentIdeas,
fetchTasks,
fetchContent,
fetchContentImages,
} from '../../services/api';
import { useToast } from '../ui/toast/ToastContainer';
import Button from '../ui/button/Button';
import {
@@ -20,6 +28,7 @@ interface CurrentProcessingCardProps {
currentRun: AutomationRun;
onUpdate: () => void;
onClose: () => void;
pipelineOverview?: PipelineStage[];
}
const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
@@ -28,12 +37,17 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
currentRun,
onUpdate,
onClose,
pipelineOverview,
}) => {
const [processingState, setProcessingState] = useState<ProcessingState | null>(null);
const [error, setError] = useState<string | null>(null);
const [isPausing, setIsPausing] = useState(false);
const [isResuming, setIsResuming] = useState(false);
const [isCancelling, setIsCancelling] = useState(false);
const [fetchedCurrently, setFetchedCurrently] = useState<ProcessingState['currently_processing']>([]);
const [fetchedUpNext, setFetchedUpNext] = useState<ProcessingState['up_next']>([]);
const [isLocallyPaused, setIsLocallyPaused] = useState(false);
const [showDebugTable, setShowDebugTable] = useState(false);
const toast = useToast();
useEffect(() => {
@@ -42,7 +56,7 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
const fetchState = async () => {
try {
const state = await automationService.getCurrentProcessing(siteId, runId);
console.debug('getCurrentProcessing response for run', runId, state);
if (!isMounted) return;
setProcessingState(state);
@@ -59,8 +73,8 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
}
};
// Only fetch if status is running or paused
if (currentRun.status === 'running' || currentRun.status === 'paused') {
// Only fetch if status is running or paused and not locally paused
if (!isLocallyPaused && (currentRun.status === 'running' || currentRun.status === 'paused')) {
// Initial fetch
fetchState();
@@ -76,13 +90,98 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
return () => {
isMounted = false;
};
}, [siteId, runId, currentRun.status, onUpdate]);
}, [siteId, runId, currentRun.status, currentRun.current_stage, onUpdate, isLocallyPaused]);
// Attempt to fetch example items for the current stage when the API does not provide up_next/currently_processing
useEffect(() => {
let isMounted = true;
const stageNumber = currentRun.current_stage;
const loadStageQueue = async () => {
try {
switch (stageNumber) {
case 1: {
const res = await fetchKeywords({ page_size: 5, site_id: siteId, status: 'new' });
if (!isMounted) return;
const items = (res.results || []).map((r: any) => ({ id: r.id, title: r.title || r.name || String(r.id), type: 'keyword' }));
setFetchedUpNext(items);
setFetchedCurrently(items.slice(0, 1));
break;
}
case 2: {
const res = await fetchClusters({ page_size: 5, site_id: siteId, status: 'new' });
if (!isMounted) return;
const items = (res.results || []).map((r: any) => ({ id: r.id, title: r.name || String(r.id), type: 'cluster' }));
setFetchedUpNext(items);
setFetchedCurrently(items.slice(0, 1));
break;
}
case 3: {
const res = await fetchContentIdeas({ page_size: 5, site_id: siteId, status: 'queued' });
if (!isMounted) return;
const items = (res.results || []).map((r: any) => ({ id: r.id, title: r.title || String(r.id), type: 'idea' }));
setFetchedUpNext(items);
setFetchedCurrently(items.slice(0, 1));
break;
}
case 4: {
// Tasks -> Content (show queued tasks)
try {
const res = await fetchTasks({ page_size: 5, site_id: siteId, status: 'queued' });
if (!isMounted) return;
const items = (res.results || []).map((r: any) => ({ id: r.id, title: r.title || r.name || String(r.id), type: 'task' }));
setFetchedUpNext(items);
setFetchedCurrently(items.slice(0, 1));
} catch (e) {
// ignore
}
break;
}
case 5: {
// Content -> Image Prompts (show content items awaiting prompts)
try {
const res = await fetchContent({ page_size: 5, site_id: siteId, status: 'queued' });
if (!isMounted) return;
const items = (res.results || []).map((r: any) => ({ id: r.id, title: r.title || r.name || String(r.id), type: 'content' }));
setFetchedUpNext(items);
setFetchedCurrently(items.slice(0, 1));
} catch (e) {
// ignore
}
break;
}
case 6: {
const res = await fetchContentImages({ page_size: 5, site_id: siteId, status: 'pending' });
if (!isMounted) return;
const items = (res.results || []).map((r: any) => ({ id: r.id, title: r.filename || String(r.id), type: 'image' }));
setFetchedUpNext(items);
setFetchedCurrently(items.slice(0, 1));
break;
}
default:
// For stages without a clear read API, clear fetched lists
setFetchedUpNext([]);
setFetchedCurrently([]);
}
} catch (err) {
console.warn('Failed to fetch stage queue samples:', err);
}
};
// Only attempt when there's no live up_next data
if ((!processingState || (processingState && (processingState.up_next || []).length === 0)) && currentRun.status === 'running') {
loadStageQueue();
}
return () => { isMounted = false; };
}, [siteId, currentRun.current_stage, currentRun.status]);
const handlePause = async () => {
setIsPausing(true);
try {
await automationService.pause(siteId, runId);
toast?.success('Automation pausing... will complete current item');
// Optimistically mark paused locally so UI stays paused until backend confirms
setIsLocallyPaused(true);
// Trigger update to refresh run status
setTimeout(onUpdate, 1000);
} catch (error: any) {
@@ -97,6 +196,8 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
try {
await automationService.resume(siteId, runId);
toast?.success('Automation resumed');
// Clear local paused flag
setIsLocallyPaused(false);
// Trigger update to refresh run status
setTimeout(onUpdate, 1000);
} catch (error: any) {
@@ -153,13 +254,67 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
);
}
if (!processingState && currentRun.status === 'running') {
return null;
}
// Build a fallback processing state from currentRun and pipelineOverview when API doesn't return live state
const currentStageIndex = (currentRun.current_stage || 1) - 1;
const stageOverview = pipelineOverview && pipelineOverview[currentStageIndex] ? pipelineOverview[currentStageIndex] : null;
const stageResult = (currentRun as any)[`stage_${currentRun.current_stage}_result`];
const percentage = processingState?.percentage || 0;
const fallbackState: ProcessingState | null = ((): ProcessingState | null => {
if (!processingState && (stageOverview || stageResult)) {
const processed = stageResult ? Object.values(stageResult).reduce((s: number, v: any) => typeof v === 'number' ? s + v : s, 0) : 0;
const total = (stageOverview?.pending || 0) + processed;
const percentage = total > 0 ? Math.round((processed / total) * 100) : 0;
return {
stage_number: currentRun.current_stage,
stage_name: stageOverview?.name || `Stage ${currentRun.current_stage}`,
stage_type: stageOverview?.type || 'AI',
total_items: total,
processed_items: processed,
percentage,
currently_processing: [],
up_next: [],
remaining_count: Math.max(0, total - processed),
};
}
return null;
})();
const displayState = processingState || fallbackState;
// If we don't have a live displayState, keep rendering the card using computed values
// Computed processed/total (use processingState when available, otherwise derive from stageResult + overview)
const computedProcessed = ((): number => {
if (displayState && typeof displayState.processed_items === 'number') return displayState.processed_items;
if (stageResult) {
// Sum numeric values in stageResult as a heuristic for processed count
return Object.values(stageResult).reduce((s: number, v: any) => (typeof v === 'number' ? s + v : s), 0);
}
return 0;
})();
const computedTotal = ((): number => {
if (displayState && typeof displayState.total_items === 'number' && displayState.total_items > 0) return displayState.total_items;
const pending = stageOverview?.pending ?? 0;
return Math.max(pending + computedProcessed, 0);
})();
const percentage = computedTotal > 0 ? Math.round((computedProcessed / computedTotal) * 100) : 0;
const isPaused = currentRun.status === 'paused';
// Choose stage accent color (simple map matching AutomationPage STAGE_CONFIG)
const stageColors = [
'from-blue-500 to-blue-600',
'from-purple-500 to-purple-600',
'from-indigo-500 to-indigo-600',
'from-green-500 to-green-600',
'from-amber-500 to-amber-600',
'from-pink-500 to-pink-600',
'from-teal-500 to-teal-600',
];
const stageColorClass = stageColors[(currentRun.current_stage || 1) - 1] || 'from-blue-500 to-blue-600';
return (
<div className={`border-2 rounded-lg p-6 mb-6 ${
isPaused
@@ -182,32 +337,55 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
{isPaused ? 'Automation Paused' : 'Automation In Progress'}
</h2>
{processingState && (
<p className="text-sm text-gray-600 dark:text-gray-400">
Stage {currentRun.current_stage}: {processingState.stage_name}
<span className={`ml-2 px-2 py-0.5 rounded text-xs ${
isPaused
? 'bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300'
: 'bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300'
}`}>
{processingState.stage_type}
</span>
</p>
{/* Centered stage row + dynamic action text */}
{displayState && (
<div className="mt-2">
<div className="text-center text-sm font-medium text-gray-700 dark:text-gray-300">
Stage {currentRun.current_stage}: {displayState.stage_name}
<span className={`ml-2 px-2 py-0.5 rounded text-xs ${
isPaused
? 'bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300'
: 'bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300'
}`}>
{displayState.stage_type}
</span>
</div>
<div className="text-center text-sm text-gray-600 dark:text-gray-400 mt-1">
{(() => {
// Build dynamic action text based on stage type and counts
const verb = displayState.stage_type === 'AI' ? 'Generating' : 'Processing';
// target label for the current stage (what is being produced)
const targetLabelMap: Record<number, string> = {
1: 'Clusters',
2: 'Ideas',
3: 'Tasks',
4: 'Content',
5: 'Image Prompts',
6: 'Images',
7: 'Review',
};
const label = targetLabelMap[displayState.stage_number] || 'Items';
return `${verb} ${computedProcessed}/${computedTotal} ${label}`;
})()}
</div>
</div>
)}
</div>
</div>
{/* Progress Info */}
{processingState && (
{displayState && (
<>
<div className="mb-4">
<div className="flex items-center justify-between mb-2">
<div className="text-3xl font-bold text-blue-600 dark:text-blue-400">
{percentage}%
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
{processingState.processed_items}/{processingState.total_items} completed
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
{computedProcessed}/{computedTotal} completed
</div>
</div>
{/* Progress Bar */}
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-3">
@@ -230,8 +408,8 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
Currently Processing:
</h3>
<div className="space-y-1">
{processingState.currently_processing.length > 0 ? (
processingState.currently_processing.map((item, idx) => (
{((displayState.currently_processing && displayState.currently_processing.length > 0) ? displayState.currently_processing : fetchedCurrently).length > 0 ? (
((displayState.currently_processing && displayState.currently_processing.length > 0) ? displayState.currently_processing : fetchedCurrently).map((item, idx) => (
<div key={idx} className="flex items-start gap-2 text-sm">
<span className={isPaused ? 'text-yellow-600 dark:text-yellow-400 mt-1' : 'text-blue-600 dark:text-blue-400 mt-1'}></span>
<span className="text-gray-800 dark:text-gray-200 font-medium line-clamp-2">
@@ -253,9 +431,9 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
Up Next:
</h3>
<div className="space-y-1">
{processingState.up_next.length > 0 ? (
{((displayState.up_next && displayState.up_next.length > 0) ? displayState.up_next : fetchedUpNext).length > 0 ? (
<>
{processingState.up_next.map((item, idx) => (
{((displayState.up_next && displayState.up_next.length > 0) ? displayState.up_next : fetchedUpNext).map((item, idx) => (
<div key={idx} className="flex items-start gap-2 text-sm">
<span className="text-gray-400 dark:text-gray-500 mt-1"></span>
<span className="text-gray-600 dark:text-gray-400 line-clamp-2">
@@ -263,9 +441,9 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
</span>
</div>
))}
{processingState.remaining_count > processingState.up_next.length + processingState.currently_processing.length && (
{displayState.remaining_count > ((displayState.up_next?.length || 0) + (displayState.currently_processing?.length || 0)) && (
<div className="text-xs text-gray-500 dark:text-gray-400 mt-2">
+ {processingState.remaining_count - processingState.up_next.length - processingState.currently_processing.length} more in queue
+ {displayState.remaining_count - ((displayState.up_next?.length || 0) + (displayState.currently_processing?.length || 0))} more in queue
</div>
)}
</>
@@ -286,8 +464,8 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
disabled={isPausing}
variant="secondary"
size="sm"
startIcon={<PauseIcon className="w-4 h-4" />}
>
<PauseIcon className="w-4 h-4 mr-2" />
{isPausing ? 'Pausing...' : 'Pause'}
</Button>
) : currentRun.status === 'paused' ? (
@@ -296,8 +474,8 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
disabled={isResuming}
variant="primary"
size="sm"
startIcon={<PlayIcon className="w-4 h-4" />}
>
<PlayIcon className="w-4 h-4 mr-2" />
{isResuming ? 'Resuming...' : 'Resume'}
</Button>
) : null}
@@ -307,8 +485,8 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
disabled={isCancelling}
variant="danger"
size="sm"
startIcon={<XMarkIcon className="w-4 h-4" />}
>
<XMarkIcon className="w-4 h-4 mr-2" />
{isCancelling ? 'Cancelling...' : 'Cancel'}
</Button>
</div>
@@ -320,68 +498,102 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
<div className="w-64 flex-shrink-0">
{/* Close Button */}
<div className="flex justify-end mb-4">
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
title="Close (card will remain available below)"
>
<XMarkIcon className="w-6 h-6" />
</button>
<Button variant="ghost" size="sm" onClick={onClose} startIcon={<XMarkIcon className="w-4 h-4" />}>
Close
</Button>
</div>
{/* Metrics Cards */}
<div className="space-y-3">
{/* Duration */}
<div className="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-2 mb-1">
<ClockIcon className="w-4 h-4 text-gray-500" />
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold">
Duration
<div className="flex items-center justify-between mb-0">
<div className="flex items-center gap-2">
<ClockIcon className="w-4 h-4 text-gray-500" />
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold">Duration</div>
</div>
</div>
<div className="text-xl font-bold text-gray-900 dark:text-white">
{formatDuration(currentRun.started_at)}
<div className="text-sm font-bold text-gray-900 dark:text-white">{formatDuration(currentRun.started_at)}</div>
</div>
</div>
{/* Credits Used */}
<div className="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-2 mb-1">
<BoltIcon className="w-4 h-4 text-amber-500" />
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold">
Credits Used
<div className="flex items-center justify-between mb-0">
<div className="flex items-center gap-2">
<BoltIcon className="w-4 h-4 text-amber-500" />
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold">Credits Used</div>
</div>
</div>
<div className="text-xl font-bold text-amber-600 dark:text-amber-400">
{currentRun.total_credits_used}
<div className="text-sm font-bold text-amber-600 dark:text-amber-400">{currentRun.total_credits_used}</div>
</div>
</div>
{/* Current Stage */}
<div className="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold mb-1">
Stage
</div>
<div className="text-xl font-bold text-gray-900 dark:text-white">
{currentRun.current_stage} of 7
<div className="flex items-center justify-between mb-0">
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold">Stage</div>
<div className="text-sm font-bold text-gray-900 dark:text-white">{currentRun.current_stage} of 7</div>
</div>
</div>
{/* Status */}
<div className="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold mb-1">
Status
</div>
<div className={`text-sm font-semibold ${
isPaused
? 'text-yellow-600 dark:text-yellow-400'
: 'text-blue-600 dark:text-blue-400'
}`}>
{isPaused ? 'Paused' : 'Running'}
<div className="flex items-center justify-between mb-0">
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold">Status</div>
<div className={`text-sm font-semibold ${isPaused ? 'text-yellow-600 dark:text-yellow-400' : 'text-blue-600 dark:text-blue-400'}`}>
{isPaused ? 'Paused' : 'Running'}
</div>
</div>
</div>
</div>
</div>
{/* Debug table toggle + table for stage data */}
<div className="mt-4">
<button
type="button"
onClick={() => setShowDebugTable(!showDebugTable)}
className="text-xs text-slate-600 hover:underline"
>
{showDebugTable ? 'Hide' : 'Show'} debug table
</button>
{showDebugTable && (
<div className="mt-3 bg-white dark:bg-gray-800 p-3 rounded border">
<div className="text-sm font-semibold mb-2">Stage Data</div>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="text-left">
<th className="pr-4">Stage</th>
<th className="pr-4">Pending</th>
<th className="pr-4">Processed</th>
<th className="pr-4">Total</th>
<th className="pr-4">Currently (sample)</th>
<th className="pr-4">Up Next (sample)</th>
</tr>
</thead>
<tbody>
{(pipelineOverview || []).map((stage) => {
const result = (currentRun as any)[`stage_${stage.number}_result`];
const processed = result ? Object.values(result).reduce((s: number, v: any) => typeof v === 'number' ? s + v : s, 0) : 0;
const total = Math.max((stage.pending || 0) + processed, 0);
const currently = currentRun.current_stage === stage.number ? (processingState?.currently_processing?.slice(0,3) || fetchedCurrently) : [];
const upnext = currentRun.current_stage === stage.number ? (processingState?.up_next?.slice(0,5) || fetchedUpNext) : [];
return (
<tr key={stage.number} className="border-t">
<td className="py-2">{stage.number} {stage.name}</td>
<td className="py-2">{stage.pending}</td>
<td className="py-2">{processed}</td>
<td className="py-2">{total}</td>
<td className="py-2">{currently.map(c => c.title).join(', ') || '-'}</td>
<td className="py-2">{upnext.map(u => u.title).join(', ') || '-'}</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
)}
</div>
</div>
</div>
);

View File

@@ -140,10 +140,10 @@ export default function FormModal({
})()}
</div>
)}
{fields.filter(f => f.key !== 'keyword' && f.key !== 'volume' && f.key !== 'difficulty').map((field) => {
{fields.filter(f => f.key !== 'keyword' && f.key !== 'volume' && f.key !== 'difficulty').map((field, idx) => {
if (field.type === 'select') {
return (
<div key={field.key}>
<div key={`${field.key}-${idx}`}>
<Label className="mb-2">
{field.label}
{field.required && <span className="text-error-500 ml-1">*</span>}
@@ -160,7 +160,7 @@ export default function FormModal({
}
if (field.type === 'textarea') {
return (
<div key={field.key}>
<div key={`${field.key}-${idx}`}>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{field.label}
{field.required && <span className="text-error-500 ml-1">*</span>}
@@ -177,7 +177,7 @@ export default function FormModal({
);
}
return (
<div key={field.key}>
<div key={`${field.key}-${idx}`}>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{field.label}
{field.required && <span className="text-error-500 ml-1">*</span>}

View File

@@ -147,15 +147,13 @@ const AdminBilling: React.FC = () => {
Admin controls for credits, pricing, and user billing
</p>
</div>
<a
href="/admin/igny8_core/"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700"
<Button
variant="outline"
startIcon={<PlugInIcon className="w-4 h-4" />}
onClick={() => window.open('/admin/igny8_core/', '_blank')}
>
<PlugInIcon className="w-4 h-4 mr-2" />
Django Admin
</a>
</Button>
</div>
{/* System Stats */}
@@ -163,30 +161,26 @@ const AdminBilling: React.FC = () => {
<EnhancedMetricCard
title="Total Users"
value={stats?.total_users || 0}
icon={UserIcon}
color="blue"
iconColor="text-blue-500"
icon={<UserIcon />}
accentColor="blue"
/>
<EnhancedMetricCard
title="Active Users"
value={stats?.active_users || 0}
icon={CheckCircleIcon}
color="green"
iconColor="text-green-500"
icon={<CheckCircleIcon />}
accentColor="green"
/>
<EnhancedMetricCard
title="Credits Issued"
value={stats?.total_credits_issued || 0}
icon={DollarLineIcon}
color="amber"
iconColor="text-amber-500"
icon={<DollarLineIcon />}
accentColor="orange"
/>
<EnhancedMetricCard
title="Credits Used"
value={stats?.total_credits_used || 0}
icon={BoltIcon}
color="purple"
iconColor="text-purple-500"
icon={<BoltIcon />}
accentColor="purple"
/>
</div>
@@ -231,28 +225,28 @@ const AdminBilling: React.FC = () => {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<ComponentCard title="Quick Actions">
<div className="space-y-3">
<Button
variant="primary"
<Button
variant="primary"
fullWidth
startIcon={<UserIcon className="w-4 h-4" />}
onClick={() => setActiveTab('users')}
>
<UserIcon className="w-4 h-4 mr-2" />
Manage User Credits
</Button>
<Button
variant="secondary"
<Button
variant="secondary"
fullWidth
startIcon={<DollarLineIcon className="w-4 h-4" />}
onClick={() => setActiveTab('pricing')}
>
<DollarLineIcon className="w-4 h-4 mr-2" />
Update Credit Costs
</Button>
<Button
variant="outline"
<Button
variant="outline"
fullWidth
startIcon={<PlugInIcon className="w-4 h-4" />}
onClick={() => window.open('/admin/igny8_core/creditcostconfig/', '_blank')}
>
<PlugInIcon className="w-4 h-4 mr-2" />
Full Admin Panel
</Button>
</div>
@@ -309,7 +303,7 @@ const AdminBilling: React.FC = () => {
</div>
</td>
<td className="px-4 py-4 whitespace-nowrap">
<Badge variant="info">{user.subscription_plan || 'Free'}</Badge>
<Badge tone="info">{user.subscription_plan || 'Free'}</Badge>
</td>
<td className="px-4 py-4 whitespace-nowrap text-right font-bold text-amber-600 dark:text-amber-400">
{user.credits}
@@ -432,7 +426,7 @@ const AdminBilling: React.FC = () => {
{config.cost}
</td>
<td className="px-6 py-4 whitespace-nowrap text-center">
<Badge variant={config.is_active ? 'success' : 'warning'}>
<Badge tone={config.is_active ? 'success' : 'warning'}>
{config.is_active ? 'Active' : 'Inactive'}
</Badge>
</td>

View File

@@ -54,6 +54,7 @@ const AutomationPage: React.FC = () => {
const [pipelineOverview, setPipelineOverview] = useState<PipelineStage[]>([]);
const [metrics, setMetrics] = useState<any>(null);
const [showConfigModal, setShowConfigModal] = useState(false);
const [showProcessingCard, setShowProcessingCard] = useState<boolean>(true);
const [loading, setLoading] = useState(true);
const [estimate, setEstimate] = useState<{ estimated_credits: number; current_balance: number; sufficient: boolean } | null>(null);
@@ -147,6 +148,10 @@ const AutomationPage: React.FC = () => {
setCurrentRun(runData.run);
setEstimate(estimateData);
setPipelineOverview(pipelineData.stages);
// show processing card when there's a current run
if (runData.run) {
setShowProcessingCard(true);
}
} catch (error: any) {
toast.error('Failed to load automation data');
console.error(error);
@@ -160,6 +165,8 @@ const AutomationPage: React.FC = () => {
try {
const data = await automationService.getCurrentRun(activeSite.id);
setCurrentRun(data.run);
// ensure processing card is visible when a run exists
if (data.run) setShowProcessingCard(true);
} catch (error) {
console.error('Failed to poll current run', error);
}
@@ -251,22 +258,27 @@ const AutomationPage: React.FC = () => {
};
const handlePause = async () => {
if (!currentRun) return;
if (!currentRun || !activeSite) return;
try {
await automationService.pause(currentRun.run_id);
await automationService.pause(activeSite.id, currentRun.run_id);
toast.success('Automation paused');
loadCurrentRun();
// refresh run and pipeline/metrics
await loadCurrentRun();
await loadPipelineOverview();
await loadMetrics();
} catch (error) {
toast.error('Failed to pause automation');
}
};
const handleResume = async () => {
if (!currentRun) return;
if (!currentRun || !activeSite) return;
try {
await automationService.resume(currentRun.run_id);
await automationService.resume(activeSite.id, currentRun.run_id);
toast.success('Automation resumed');
loadCurrentRun();
await loadCurrentRun();
await loadPipelineOverview();
await loadMetrics();
} catch (error) {
toast.error('Failed to resume automation');
}
@@ -278,8 +290,13 @@ const AutomationPage: React.FC = () => {
await automationService.updateConfig(activeSite.id, newConfig);
toast.success('Configuration saved');
setShowConfigModal(false);
loadData();
// Optimistically update config locally and refresh data
setConfig((prev) => ({ ...(prev as AutomationConfig), ...newConfig } as AutomationConfig));
await loadPipelineOverview();
await loadMetrics();
await loadCurrentRun();
} catch (error) {
console.error('Failed to save config:', error);
toast.error('Failed to save configuration');
}
};
@@ -665,18 +682,21 @@ const AutomationPage: React.FC = () => {
</div>
{/* Current Processing Card - Shows real-time automation progress */}
{currentRun && (currentRun.status === 'running' || currentRun.status === 'paused') && activeSite && (
{currentRun && showProcessingCard && activeSite && (
<CurrentProcessingCard
runId={currentRun.run_id}
siteId={activeSite.id}
currentRun={currentRun}
onUpdate={() => {
// Refresh current run status
loadCurrentRun();
pipelineOverview={pipelineOverview}
onUpdate={async () => {
// Refresh current run status, pipeline overview and metrics (no full page reload)
await loadCurrentRun();
await loadPipelineOverview();
await loadMetrics();
}}
onClose={() => {
// Card will remain in DOM but user acknowledged it
// Can add state here to minimize it if needed
// hide the processing card until next run
setShowProcessingCard(false);
}}
/>
)}
@@ -692,7 +712,8 @@ const AutomationPage: React.FC = () => {
const isComplete = currentRun && currentRun.current_stage > stage.number;
const result = currentRun ? (currentRun[`stage_${stage.number}_result` as keyof AutomationRun] as any) : null;
const processed = result ? Object.values(result).reduce((sum: number, val) => typeof val === 'number' ? sum + val : sum, 0) : 0;
const progressPercent = stage.pending > 0 ? Math.round((processed / (processed + stage.pending)) * 100) : 0;
const total = (stage.pending ?? 0) + processed;
const progressPercent = total > 0 ? Math.round((processed / total) * 100) : 0;
return (
<div
@@ -787,7 +808,8 @@ const AutomationPage: React.FC = () => {
const isComplete = currentRun && currentRun.current_stage > stage.number;
const result = currentRun ? (currentRun[`stage_${stage.number}_result` as keyof AutomationRun] as any) : null;
const processed = result ? Object.values(result).reduce((sum: number, val) => typeof val === 'number' ? sum + val : sum, 0) : 0;
const progressPercent = stage.pending > 0 ? Math.round((processed / (processed + stage.pending)) * 100) : 0;
const total = (stage.pending ?? 0) + processed;
const progressPercent = total > 0 ? Math.round((processed / total) * 100) : 0;
return (
<div

View File

@@ -113,11 +113,14 @@ const CreditsAndBilling: React.FC = () => {
Manage your credits, view transactions, and monitor usage
</p>
</div>
<Button variant="primary" onClick={() => {
// TODO: Link to purchase credits page
toast?.info('Purchase credits feature coming soon');
}}>
<DollarLineIcon className="w-4 h-4 mr-2" />
<Button
variant="primary"
startIcon={<DollarLineIcon className="w-4 h-4" />}
onClick={() => {
// TODO: Link to purchase credits page
toast?.info('Purchase credits feature coming soon');
}}
>
Purchase Credits
</Button>
</div>
@@ -127,31 +130,27 @@ const CreditsAndBilling: React.FC = () => {
<EnhancedMetricCard
title="Current Balance"
value={balance?.credits || 0}
icon={BoltIcon}
color="amber"
iconColor="text-amber-500"
icon={<BoltIcon />}
accentColor="orange"
/>
<EnhancedMetricCard
title="Monthly Included"
value={balance?.monthly_credits_included || 0}
subtitle={balance?.subscription_plan || 'Free'}
icon={CheckCircleIcon}
color="green"
iconColor="text-green-500"
icon={<CheckCircleIcon />}
accentColor="green"
/>
<EnhancedMetricCard
title="Bonus Credits"
value={balance?.bonus_credits || 0}
icon={DollarLineIcon}
color="blue"
iconColor="text-blue-500"
icon={<DollarLineIcon />}
accentColor="blue"
/>
<EnhancedMetricCard
title="Total This Month"
value={usageLogs.reduce((sum, log) => sum + log.credits_used, 0)}
icon={TimeIcon}
color="purple"
iconColor="text-purple-500"
icon={<TimeIcon />}
accentColor="purple"
/>
</div>
@@ -201,7 +200,7 @@ const CreditsAndBilling: React.FC = () => {
<div key={transaction.id} className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="flex-1">
<div className="flex items-center gap-2">
<Badge variant={getTransactionTypeColor(transaction.transaction_type)}>
<Badge tone={getTransactionTypeColor(transaction.transaction_type) as any}>
{transaction.transaction_type}
</Badge>
<span className="text-sm text-gray-900 dark:text-white">
@@ -290,7 +289,7 @@ const CreditsAndBilling: React.FC = () => {
{new Date(transaction.created_at).toLocaleDateString()}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<Badge variant={getTransactionTypeColor(transaction.transaction_type)}>
<Badge tone={getTransactionTypeColor(transaction.transaction_type) as any}>
{transaction.transaction_type}
</Badge>
</td>