This commit is contained in:
IGNY8 VPS (Salman)
2025-12-04 17:58:41 +00:00
parent 40dfe20ead
commit 1521f3ff8c
13 changed files with 506 additions and 120 deletions

View File

@@ -117,7 +117,7 @@ class AutomationLogger:
Returns:
List of log lines (newest first)
"""
log_file = os.path.join(self._get_run_dir(account_id, site_id, run_id), 'automation_run.log')
log_file = os.path.join(self._get_run_dir(account_id, site_id, str(run_id)), 'automation_run.log')
if not os.path.exists(log_file):
return []

View File

@@ -218,6 +218,29 @@ class AutomationService:
keyword_ids = list(pending_keywords.values_list('id', flat=True))
for i in range(0, len(keyword_ids), actual_batch_size):
# Check if automation should stop (paused or cancelled)
should_stop, reason = self._check_should_stop()
if should_stop:
self.logger.log_stage_progress(
self.run.run_id, self.account.id, self.site.id,
stage_number, f"Stage {reason} - saving progress ({keywords_processed} keywords processed)"
)
# Save current progress
credits_used = self._get_credits_used() - credits_before
time_elapsed = self._format_time_elapsed(start_time)
self.run.stage_1_result = {
'keywords_processed': keywords_processed,
'clusters_created': clusters_created,
'batches_run': batches_run,
'credits_used': credits_used,
'time_elapsed': time_elapsed,
'partial': True,
'stopped_reason': reason
}
self.run.total_credits_used += credits_used
self.run.save()
return
try:
batch = keyword_ids[i:i + actual_batch_size]
batch_num = (i // actual_batch_size) + 1
@@ -354,13 +377,11 @@ class AutomationService:
stage_number, "Pre-stage validation passed: 0 keywords pending from Stage 1"
)
# Query clusters without ideas
# Query clusters with status='new'
pending_clusters = Clusters.objects.filter(
site=self.site,
status='new',
disabled=False
).exclude(
ideas__isnull=False
)
total_count = pending_clusters.count()
@@ -386,6 +407,33 @@ class AutomationService:
credits_before = self._get_credits_used()
for cluster in pending_clusters:
# Check if automation should stop (paused or cancelled)
should_stop, reason = self._check_should_stop()
if should_stop:
self.logger.log_stage_progress(
self.run.run_id, self.account.id, self.site.id,
stage_number, f"Stage {reason} - saving progress ({clusters_processed} clusters processed)"
)
# Save current progress
ideas_created = ContentIdeas.objects.filter(
site=self.site,
created_at__gte=self.run.started_at
).count()
credits_used = self._get_credits_used() - credits_before
time_elapsed = self._format_time_elapsed(start_time)
from django.utils import timezone
self.run.stage_2_result = {
'clusters_processed': clusters_processed,
'ideas_created': ideas_created,
'credits_used': credits_used,
'time_elapsed': time_elapsed,
'partial': True,
'stopped_reason': reason
}
self.run.total_credits_used += credits_used
self.run.save()
return
try:
self.logger.log_stage_progress(
self.run.run_id, self.account.id, self.site.id,
@@ -523,6 +571,29 @@ class AutomationService:
idea_list = list(pending_ideas)
for i in range(0, len(idea_list), batch_size):
# Check if automation should stop (paused or cancelled)
should_stop, reason = self._check_should_stop()
if should_stop:
self.logger.log_stage_progress(
self.run.run_id, self.account.id, self.site.id,
stage_number, f"Stage {reason} - saving progress ({ideas_processed} ideas processed, {tasks_created} tasks created)"
)
# Save current progress
time_elapsed = self._format_time_elapsed(start_time)
from django.utils import timezone
self.run.stage_3_result = {
'ideas_processed': ideas_processed,
'tasks_created': tasks_created,
'batches_run': batches_run,
'started_at': self.run.started_at.isoformat(),
'completed_at': timezone.now().isoformat(),
'time_elapsed': time_elapsed,
'partial': True,
'stopped_reason': reason
}
self.run.save()
return
batch = idea_list[i:i + batch_size]
batch_num = (i // batch_size) + 1
total_batches = (len(idea_list) + batch_size - 1) // batch_size
@@ -658,6 +729,33 @@ class AutomationService:
total_tasks = len(task_list)
for idx, task in enumerate(task_list, 1):
# Check if automation should stop (paused or cancelled)
should_stop, reason = self._check_should_stop()
if should_stop:
self.logger.log_stage_progress(
self.run.run_id, self.account.id, self.site.id,
stage_number, f"Stage {reason} - saving progress ({tasks_processed} tasks processed)"
)
# Save current progress
content_created = Content.objects.filter(
site=self.site,
created_at__gte=self.run.started_at
).count()
credits_used = self._get_credits_used() - credits_before
time_elapsed = self._format_time_elapsed(start_time)
from django.utils import timezone
self.run.stage_4_result = {
'tasks_processed': tasks_processed,
'content_created': content_created,
'credits_used': credits_used,
'time_elapsed': time_elapsed,
'partial': True,
'stopped_reason': reason
}
self.run.total_credits_used += credits_used
self.run.save()
return
try:
self.logger.log_stage_progress(
self.run.run_id, self.account.id, self.site.id,
@@ -834,6 +932,33 @@ class AutomationService:
total_content = len(content_list)
for idx, content in enumerate(content_list, 1):
# Check if automation should stop (paused or cancelled)
should_stop, reason = self._check_should_stop()
if should_stop:
self.logger.log_stage_progress(
self.run.run_id, self.account.id, self.site.id,
stage_number, f"Stage {reason} - saving progress ({content_processed} content processed)"
)
# Save current progress
prompts_created = Images.objects.filter(
site=self.site,
created_at__gte=self.run.started_at
).count()
credits_used = self._get_credits_used() - credits_before
time_elapsed = self._format_time_elapsed(start_time)
from django.utils import timezone
self.run.stage_5_result = {
'content_processed': content_processed,
'prompts_created': prompts_created,
'credits_used': credits_used,
'time_elapsed': time_elapsed,
'partial': True,
'stopped_reason': reason
}
self.run.total_credits_used += credits_used
self.run.save()
return
try:
self.logger.log_stage_progress(
self.run.run_id, self.account.id, self.site.id,
@@ -981,6 +1106,40 @@ class AutomationService:
total_images = len(image_list)
for idx, image in enumerate(image_list, 1):
# Check if automation should stop (paused or cancelled)
should_stop, reason = self._check_should_stop()
if should_stop:
self.logger.log_stage_progress(
self.run.run_id, self.account.id, self.site.id,
stage_number, f"Stage {reason} - saving progress ({images_processed} images processed)"
)
# Save current progress
images_generated = Images.objects.filter(
site=self.site,
status='completed',
created_at__gte=self.run.started_at
).count()
content_moved = Content.objects.filter(
site=self.site,
status='review',
updated_at__gte=self.run.started_at
).count()
credits_used = self._get_credits_used() - credits_before
time_elapsed = self._format_time_elapsed(start_time)
from django.utils import timezone
self.run.stage_6_result = {
'images_processed': images_processed,
'images_generated': images_generated,
'content_moved_to_review': content_moved,
'credits_used': credits_used,
'time_elapsed': time_elapsed,
'partial': True,
'stopped_reason': reason
}
self.run.total_credits_used += credits_used
self.run.save()
return
try:
content_title = image.content.title if image.content else 'Unknown'
self.logger.log_stage_progress(

View File

@@ -152,12 +152,6 @@ def resume_automation_task(self, run_id: str):
# Alias for continue_automation_task (same as resume)
continue_automation_task = resume_automation_task
# Release lock
from django.core.cache import cache
cache.delete(f'automation_lock_{run.site.id}')
raise
def _calculate_next_run(config: AutomationConfig, now: datetime) -> datetime:
"""Calculate next run time based on frequency"""

View File

@@ -280,7 +280,7 @@ class AutomationViewSet(viewsets.ViewSet):
lines = int(request.query_params.get('lines', 100))
log_text = service.logger.get_activity_log(
run_id, run.account.id, run.site.id, lines
run.account.id, run.site.id, run_id, lines
)
return Response({

View File

@@ -0,0 +1,54 @@
"""
Billing API Views
Stub endpoints for billing pages
"""
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
class BillingViewSet(viewsets.ViewSet):
"""Billing endpoints"""
permission_classes = [IsAuthenticated]
@action(detail=False, methods=['get'], url_path='account_balance')
def account_balance(self, request):
"""Get user's credit balance"""
return Response({
'credits': 0,
'subscription_plan': 'Free',
'monthly_credits_included': 0,
'bonus_credits': 0
})
@action(detail=False, methods=['get'])
def transactions(self, request):
"""List credit transactions"""
return Response({
'results': [],
'count': 0
})
@action(detail=False, methods=['get'])
def usage(self, request):
"""List credit usage"""
return Response({
'results': [],
'count': 0
})
class AdminBillingViewSet(viewsets.ViewSet):
"""Admin billing endpoints"""
permission_classes = [IsAuthenticated]
@action(detail=False, methods=['get'])
def stats(self, request):
"""System billing stats"""
return Response({
'total_users': 0,
'active_users': 0,
'total_credits_issued': 0,
'total_credits_used': 0
})

View File

@@ -0,0 +1,14 @@
from django.urls import path
from rest_framework.routers import DefaultRouter
from .views import AdminBillingViewSet
router = DefaultRouter()
urlpatterns = [
path('billing/stats/', AdminBillingViewSet.as_view({'get': 'stats'}), name='admin-billing-stats'),
path('users/', AdminBillingViewSet.as_view({'get': 'list_users'}), name='admin-users-list'),
path('users/<int:user_id>/adjust-credits/', AdminBillingViewSet.as_view({'post': 'adjust_credits'}), name='admin-adjust-credits'),
path('credit-costs/', AdminBillingViewSet.as_view({'get': 'list_credit_costs', 'post': 'update_credit_costs'}), name='admin-credit-costs'),
]
urlpatterns += router.urls

View File

@@ -3,7 +3,13 @@ URL patterns for billing module
"""
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import CreditBalanceViewSet, CreditUsageViewSet, CreditTransactionViewSet
from .views import (
CreditBalanceViewSet,
CreditUsageViewSet,
CreditTransactionViewSet,
BillingOverviewViewSet,
AdminBillingViewSet
)
router = DefaultRouter()
router.register(r'credits/balance', CreditBalanceViewSet, basename='credit-balance')
@@ -12,5 +18,13 @@ router.register(r'credits/transactions', CreditTransactionViewSet, basename='cre
urlpatterns = [
path('', include(router.urls)),
# User-facing billing overview
path('account_balance/', BillingOverviewViewSet.as_view({'get': 'account_balance'}), name='account-balance'),
path('transactions/', CreditTransactionViewSet.as_view({'get': 'list'}), name='transactions'),
path('usage/', CreditUsageViewSet.as_view({'get': 'list'}), name='usage'),
# Admin billing endpoints
path('admin/billing/stats/', AdminBillingViewSet.as_view({'get': 'stats'}), name='admin-billing-stats'),
path('admin/users/', AdminBillingViewSet.as_view({'get': 'list_users'}), name='admin-users-list'),
path('admin/credit-costs/', AdminBillingViewSet.as_view({'get': 'credit_costs'}), name='admin-credit-costs'),
]

View File

@@ -404,3 +404,170 @@ class CreditTransactionViewSet(AccountModelViewSet):
return queryset.order_by('-created_at')
class BillingOverviewViewSet(viewsets.ViewSet):
"""User-facing billing overview API"""
permission_classes = [IsAuthenticatedAndActive]
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
def account_balance(self, request):
"""Get account balance with subscription info"""
account = getattr(request, 'account', None) or request.user.account
# Get subscription plan
subscription_plan = 'Free'
monthly_credits_included = 0
if account.plan:
subscription_plan = account.plan.name
monthly_credits_included = account.plan.get_effective_credits_per_month()
# Calculate bonus credits (credits beyond monthly allowance)
bonus_credits = max(0, account.credits - monthly_credits_included)
data = {
'credits': account.credits or 0,
'subscription_plan': subscription_plan,
'monthly_credits_included': monthly_credits_included,
'bonus_credits': bonus_credits,
}
return Response(data)
class AdminBillingViewSet(viewsets.ViewSet):
"""Admin-only billing management API"""
permission_classes = [IsAuthenticatedAndActive, permissions.IsAdminUser]
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
def stats(self, request):
"""Get system-wide billing statistics"""
from igny8_core.auth.models import Account
total_users = Account.objects.filter(status='active').count()
active_users = Account.objects.filter(status='active').exclude(users__last_login__isnull=True).count()
total_credits_issued = Account.objects.aggregate(
total=Sum('credits')
)['total'] or 0
total_credits_used = CreditUsageLog.objects.aggregate(
total=Sum('credits_used')
)['total'] or 0
return Response({
'total_users': total_users,
'active_users': active_users,
'total_credits_issued': total_credits_issued,
'total_credits_used': total_credits_used,
})
def list_users(self, request):
"""List all users with credit information"""
from igny8_core.auth.models import Account
from django.db.models import Q
# Get search query from request
search = request.query_params.get('search', '')
queryset = Account.objects.filter(status='active').prefetch_related('users')
# Apply search filter
if search:
queryset = queryset.filter(
Q(user__username__icontains=search) |
Q(user__email__icontains=search)
)
accounts = queryset[:100]
data = []
for acc in accounts:
user = acc.users.first() if acc.users.exists() else None
data.append({
'id': acc.id,
'username': user.username if user else 'N/A',
'email': user.email if user else 'N/A',
'credits': acc.credits or 0,
'subscription_plan': acc.plan.name if acc.plan else 'Free',
'is_active': acc.status == 'active',
'date_joined': acc.created_at
})
return Response({'results': data})
def adjust_credits(self, request, user_id):
"""Adjust credits for a specific user"""
from igny8_core.auth.models import Account
try:
account = Account.objects.get(id=user_id)
except Account.DoesNotExist:
return Response({'error': 'User not found'}, status=404)
amount = request.data.get('amount', 0)
reason = request.data.get('reason', 'Admin adjustment')
try:
amount = int(amount)
except (ValueError, TypeError):
return Response({'error': 'Invalid amount'}, status=400)
# Adjust credits
old_balance = account.credits
account.credits = (account.credits or 0) + amount
account.save()
# Log the adjustment
CreditUsageLog.objects.create(
account=account,
operation_type='admin_adjustment',
credits_used=-amount, # Negative for additions
credits_balance_after=account.credits,
details={'reason': reason, 'old_balance': old_balance, 'adjusted_by': request.user.id}
)
return Response({
'success': True,
'new_balance': account.credits,
'old_balance': old_balance,
'adjustment': amount
})
def list_credit_costs(self, request):
"""List credit cost configurations"""
from igny8_core.business.billing.models import CreditCostConfig
configs = CreditCostConfig.objects.filter(is_active=True)
data = [{
'id': c.id,
'operation_type': c.operation_type,
'display_name': c.display_name,
'credits_cost': c.credits_cost,
'unit': c.unit,
'is_active': c.is_active,
'created_at': c.created_at
} for c in configs]
return Response({'results': data})
def update_credit_costs(self, request):
"""Update credit cost configurations"""
from igny8_core.business.billing.models import CreditCostConfig
updates = request.data.get('updates', [])
for update in updates:
config_id = update.get('id')
new_cost = update.get('cost')
if config_id and new_cost is not None:
try:
config = CreditCostConfig.objects.get(id=config_id)
config.cost = new_cost
config.save()
except CreditCostConfig.DoesNotExist:
continue
return Response({'success': True})

View File

@@ -41,6 +41,7 @@ urlpatterns = [
path('api/v1/writer/', include('igny8_core.modules.writer.urls')),
path('api/v1/system/', include('igny8_core.modules.system.urls')),
path('api/v1/billing/', include('igny8_core.modules.billing.urls')), # Billing endpoints
path('api/v1/admin/', include('igny8_core.modules.billing.admin_urls')), # Admin billing
path('api/v1/automation/', include('igny8_core.business.automation.urls')), # Automation endpoints
path('api/v1/linker/', include('igny8_core.modules.linker.urls')), # Linker endpoints
path('api/v1/optimizer/', include('igny8_core.modules.optimizer.urls')), # Optimizer endpoints

View File

@@ -4,6 +4,8 @@
*/
import React, { useState } from 'react';
import { AutomationConfig } from '../../services/automationService';
import { Modal } from '../ui/modal';
import Button from '../ui/button/Button';
interface ConfigModalProps {
config: AutomationConfig;
@@ -28,14 +30,24 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSave(formData);
// Ensure delays are included in the save
const dataToSave = {
...formData,
within_stage_delay: formData.within_stage_delay || 3,
between_stage_delay: formData.between_stage_delay || 5,
};
console.log('Saving config with delays:', dataToSave);
onSave(dataToSave);
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 max-w-2xl w-full max-h-screen overflow-y-auto">
<h2 className="text-2xl font-bold mb-4">Automation Configuration</h2>
<Modal
isOpen={true}
onClose={onCancel}
className="max-w-4xl p-8"
>
<h2 className="text-2xl font-bold mb-6">Automation Configuration</h2>
<div className="max-h-[70vh] overflow-y-auto pr-2">{/* Added pr-2 for scrollbar padding */}
<form onSubmit={handleSubmit}>
{/* Enable/Disable */}
<div className="mb-4">
@@ -270,23 +282,23 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
{/* Buttons */}
<div className="flex justify-end gap-2 mt-6">
<button
<Button
type="button"
onClick={onCancel}
className="px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300"
variant="secondary"
>
Cancel
</button>
<button
</Button>
<Button
type="submit"
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
variant="primary"
>
Save Configuration
</button>
</Button>
</div>
</form>
</div>
</div>
</Modal>
);
};

View File

@@ -92,3 +92,5 @@ export const Modal: React.FC<ModalProps> = ({
</div>
);
};
export default Modal;

View File

@@ -63,7 +63,10 @@ const AutomationPage: React.FC = () => {
const interval = setInterval(() => {
if (currentRun && (currentRun.status === 'running' || currentRun.status === 'paused')) {
// When automation is running, refresh both run and metrics
loadCurrentRun();
loadPipelineOverview();
loadMetrics(); // Add metrics refresh during run
} else {
loadPipelineOverview();
}
@@ -172,6 +175,66 @@ const AutomationPage: React.FC = () => {
}
};
const loadMetrics = async () => {
if (!activeSite) return;
try {
const siteId = activeSite.id;
const [
keywordsTotalRes,
keywordsNewRes,
keywordsMappedRes,
clustersTotalRes,
clustersNewRes,
clustersMappedRes,
ideasTotalRes,
ideasNewRes,
ideasQueuedRes,
ideasCompletedRes,
tasksTotalRes,
contentTotalRes,
contentDraftRes,
contentReviewRes,
contentPublishedRes,
imagesTotalRes,
imagesPendingRes,
] = await Promise.all([
fetchKeywords({ page_size: 1, site_id: siteId }),
fetchKeywords({ page_size: 1, site_id: siteId, status: 'new' }),
fetchKeywords({ page_size: 1, site_id: siteId, status: 'mapped' }),
fetchClusters({ page_size: 1, site_id: siteId }),
fetchClusters({ page_size: 1, site_id: siteId, status: 'new' }),
fetchClusters({ page_size: 1, site_id: siteId, status: 'mapped' }),
fetchContentIdeas({ page_size: 1, site_id: siteId }),
fetchContentIdeas({ page_size: 1, site_id: siteId, status: 'new' }),
fetchContentIdeas({ page_size: 1, site_id: siteId, status: 'queued' }),
fetchContentIdeas({ page_size: 1, site_id: siteId, status: 'completed' }),
fetchTasks({ page_size: 1, site_id: siteId }),
fetchContent({ page_size: 1, site_id: siteId }),
fetchContent({ page_size: 1, site_id: siteId, status: 'draft' }),
fetchContent({ page_size: 1, site_id: siteId, status: 'review' }),
fetchContent({ page_size: 1, site_id: siteId, status: 'published' }),
fetchContentImages({ page_size: 1, site_id: siteId }),
fetchContentImages({ page_size: 1, site_id: siteId, status: 'pending' }),
]);
setMetrics({
keywords: { total: keywordsTotalRes.count || 0, new: keywordsNewRes.count || 0, mapped: keywordsMappedRes.count || 0 },
clusters: { total: clustersTotalRes.count || 0, new: clustersNewRes.count || 0, mapped: clustersMappedRes.count || 0 },
ideas: { total: ideasTotalRes.count || 0, new: ideasNewRes.count || 0, queued: ideasQueuedRes.count || 0, completed: ideasCompletedRes.count || 0 },
tasks: { total: tasksTotalRes.count || 0 },
content: {
total: contentTotalRes.count || 0,
draft: contentDraftRes.count || 0,
review: contentReviewRes.count || 0,
published: contentPublishedRes.count || 0,
},
images: { total: imagesTotalRes.count || 0, pending: imagesPendingRes.count || 0 },
});
} catch (e) {
console.warn('Failed to fetch metrics', e);
}
};
const handleRunNow = async () => {
if (!activeSite) return;
if (estimate && !estimate.sufficient) {
@@ -887,103 +950,9 @@ const AutomationPage: React.FC = () => {
</div>
</div>
{/* Status Summary Card */}
{currentRun && (
<div className="relative rounded-xl border-2 border-slate-300 dark:border-gray-700 p-5 bg-gradient-to-br from-slate-100 to-slate-200 dark:from-gray-800/50 dark:to-gray-700/50">
<div className="mb-3">
<div className="text-sm font-bold text-gray-900 dark:text-white mb-1">Current Status</div>
<div className="text-xs font-medium text-gray-600 dark:text-gray-400">Run Summary</div>
</div>
<div className="space-y-2 text-xs">
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">Run ID:</span>
<span className="font-mono text-xs text-slate-900 dark:text-white">{currentRun.run_id.split('_').pop()}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">Started:</span>
<span className="font-semibold text-slate-900 dark:text-white">
{new Date(currentRun.started_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">Current Stage:</span>
<span className="font-bold text-blue-600 dark:text-blue-400">{currentRun.current_stage}/7</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">Credits Used:</span>
<span className="font-bold text-brand-600 dark:text-brand-400">{currentRun.total_credits_used}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">Completion:</span>
<span className="font-bold text-slate-900 dark:text-white">{Math.round((currentRun.current_stage / 7) * 100)}%</span>
</div>
</div>
<div className="mt-4 pt-3 border-t border-slate-300 dark:border-gray-600">
<div className={`
size-12 mx-auto rounded-full flex items-center justify-center
${currentRun.status === 'running'
? 'bg-gradient-to-br from-blue-500 to-blue-600 animate-pulse'
: currentRun.status === 'paused'
? 'bg-gradient-to-br from-amber-500 to-amber-600'
: 'bg-gradient-to-br from-success-500 to-success-600'
}
`}>
{currentRun.status === 'running' && <div className="size-3 bg-white rounded-full"></div>}
{currentRun.status === 'paused' && <ClockIcon className="size-6 text-white" />}
{currentRun.status === 'completed' && <CheckCircleIcon className="size-6 text-white" />}
</div>
<div className="text-center mt-2 text-xs font-semibold text-gray-700 dark:text-gray-300 capitalize">
{currentRun.status}
</div>
</div>
</div>
)}
</div>
</ComponentCard>
{/* Current Run Details */}
{currentRun && (
<ComponentCard title={`Current Run: ${currentRun.run_id}`} desc="Live automation progress and detailed results">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<EnhancedMetricCard
title="Status"
value={currentRun.status}
icon={
currentRun.status === 'running' ? <BoltIcon className="size-6" /> :
currentRun.status === 'paused' ? <ClockIcon className="size-6" /> :
currentRun.status === 'completed' ? <CheckCircleIcon className="size-6" /> :
<FileTextIcon className="size-6" />
}
accentColor={
currentRun.status === 'running' ? 'blue' :
currentRun.status === 'paused' ? 'orange' :
currentRun.status === 'completed' ? 'success' : 'red'
}
/>
<EnhancedMetricCard
title="Started"
value={new Date(currentRun.started_at).toLocaleTimeString()}
icon={<ClockIcon className="size-6" />}
accentColor="blue"
/>
<EnhancedMetricCard
title="Credits Used"
value={currentRun.total_credits_used}
icon={<BoltIcon className="size-6" />}
accentColor="blue"
/>
<EnhancedMetricCard
title="Trigger"
value={currentRun.trigger_type}
icon={<PaperPlaneIcon className="size-6" />}
accentColor="green"
/>
</div>
</ComponentCard>
)}
{/* Activity Log */}
{currentRun && <ActivityLog runId={currentRun.run_id} />}

View File

@@ -13,7 +13,7 @@ import Badge from '../../components/ui/badge/Badge';
import {
BoltIcon,
DollarLineIcon,
ClockIcon,
TimeIcon,
CheckCircleIcon
} from '../../icons';
@@ -149,7 +149,7 @@ const CreditsAndBilling: React.FC = () => {
<EnhancedMetricCard
title="Total This Month"
value={usageLogs.reduce((sum, log) => sum + log.credits_used, 0)}
icon={ClockIcon}
icon={TimeIcon}
color="purple"
iconColor="text-purple-500"
/>