From 1521f3ff8c91d37eb80c6f12e0a8f2c27db949b8 Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Thu, 4 Dec 2025 17:58:41 +0000 Subject: [PATCH] fixes --- .../automation/services/automation_logger.py | 2 +- .../automation/services/automation_service.py | 165 ++++++++++++++++- .../igny8_core/business/automation/tasks.py | 6 - .../igny8_core/business/automation/views.py | 2 +- backend/igny8_core/business/billing/views.py | 54 ++++++ .../igny8_core/modules/billing/admin_urls.py | 14 ++ backend/igny8_core/modules/billing/urls.py | 16 +- backend/igny8_core/modules/billing/views.py | 167 ++++++++++++++++++ backend/igny8_core/urls.py | 1 + .../src/components/Automation/ConfigModal.tsx | 36 ++-- frontend/src/components/ui/modal/index.tsx | 2 + .../src/pages/Automation/AutomationPage.tsx | 157 +++++++--------- .../src/pages/Settings/CreditsAndBilling.tsx | 4 +- 13 files changed, 506 insertions(+), 120 deletions(-) create mode 100644 backend/igny8_core/business/billing/views.py create mode 100644 backend/igny8_core/modules/billing/admin_urls.py diff --git a/backend/igny8_core/business/automation/services/automation_logger.py b/backend/igny8_core/business/automation/services/automation_logger.py index 317ab249..cbb6fd4b 100644 --- a/backend/igny8_core/business/automation/services/automation_logger.py +++ b/backend/igny8_core/business/automation/services/automation_logger.py @@ -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 [] diff --git a/backend/igny8_core/business/automation/services/automation_service.py b/backend/igny8_core/business/automation/services/automation_service.py index d2e2bfc1..edfcb885 100644 --- a/backend/igny8_core/business/automation/services/automation_service.py +++ b/backend/igny8_core/business/automation/services/automation_service.py @@ -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( diff --git a/backend/igny8_core/business/automation/tasks.py b/backend/igny8_core/business/automation/tasks.py index d37b54e5..b17163e3 100644 --- a/backend/igny8_core/business/automation/tasks.py +++ b/backend/igny8_core/business/automation/tasks.py @@ -151,12 +151,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: diff --git a/backend/igny8_core/business/automation/views.py b/backend/igny8_core/business/automation/views.py index 9f48eeb0..9deec648 100644 --- a/backend/igny8_core/business/automation/views.py +++ b/backend/igny8_core/business/automation/views.py @@ -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({ diff --git a/backend/igny8_core/business/billing/views.py b/backend/igny8_core/business/billing/views.py new file mode 100644 index 00000000..4f8de012 --- /dev/null +++ b/backend/igny8_core/business/billing/views.py @@ -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 + }) diff --git a/backend/igny8_core/modules/billing/admin_urls.py b/backend/igny8_core/modules/billing/admin_urls.py new file mode 100644 index 00000000..9efd28e2 --- /dev/null +++ b/backend/igny8_core/modules/billing/admin_urls.py @@ -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//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 diff --git a/backend/igny8_core/modules/billing/urls.py b/backend/igny8_core/modules/billing/urls.py index 8bc7bee2..3665f058 100644 --- a/backend/igny8_core/modules/billing/urls.py +++ b/backend/igny8_core/modules/billing/urls.py @@ -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'), ] diff --git a/backend/igny8_core/modules/billing/views.py b/backend/igny8_core/modules/billing/views.py index 376e9009..1d3f866a 100644 --- a/backend/igny8_core/modules/billing/views.py +++ b/backend/igny8_core/modules/billing/views.py @@ -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}) + diff --git a/backend/igny8_core/urls.py b/backend/igny8_core/urls.py index f65106f6..3c27dfa5 100644 --- a/backend/igny8_core/urls.py +++ b/backend/igny8_core/urls.py @@ -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 diff --git a/frontend/src/components/Automation/ConfigModal.tsx b/frontend/src/components/Automation/ConfigModal.tsx index d03cccee..8707b743 100644 --- a/frontend/src/components/Automation/ConfigModal.tsx +++ b/frontend/src/components/Automation/ConfigModal.tsx @@ -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 = ({ 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 ( -
-
-

Automation Configuration

- + +

Automation Configuration

+
{/* Added pr-2 for scrollbar padding */}
{/* Enable/Disable */}
@@ -270,23 +282,23 @@ const ConfigModal: React.FC = ({ config, onSave, onCancel }) = {/* Buttons */}
- - +
-
+
); }; diff --git a/frontend/src/components/ui/modal/index.tsx b/frontend/src/components/ui/modal/index.tsx index 4010dc48..db5f0541 100644 --- a/frontend/src/components/ui/modal/index.tsx +++ b/frontend/src/components/ui/modal/index.tsx @@ -92,3 +92,5 @@ export const Modal: React.FC = ({
); }; + +export default Modal; diff --git a/frontend/src/pages/Automation/AutomationPage.tsx b/frontend/src/pages/Automation/AutomationPage.tsx index d7a3c4a4..a6e030a0 100644 --- a/frontend/src/pages/Automation/AutomationPage.tsx +++ b/frontend/src/pages/Automation/AutomationPage.tsx @@ -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 = () => {
- {/* Status Summary Card */} - {currentRun && ( -
-
-
Current Status
-
Run Summary
-
- -
-
- Run ID: - {currentRun.run_id.split('_').pop()} -
-
- Started: - - {new Date(currentRun.started_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} - -
-
- Current Stage: - {currentRun.current_stage}/7 -
-
- Credits Used: - {currentRun.total_credits_used} -
-
- Completion: - {Math.round((currentRun.current_stage / 7) * 100)}% -
-
- -
-
- {currentRun.status === 'running' &&
} - {currentRun.status === 'paused' && } - {currentRun.status === 'completed' && } -
-
- {currentRun.status} -
-
-
- )} - {/* Current Run Details */} - {currentRun && ( - -
- : - currentRun.status === 'paused' ? : - currentRun.status === 'completed' ? : - - } - accentColor={ - currentRun.status === 'running' ? 'blue' : - currentRun.status === 'paused' ? 'orange' : - currentRun.status === 'completed' ? 'success' : 'red' - } - /> - } - accentColor="blue" - /> - } - accentColor="blue" - /> - } - accentColor="green" - /> -
-
- )} - {/* Activity Log */} {currentRun && } diff --git a/frontend/src/pages/Settings/CreditsAndBilling.tsx b/frontend/src/pages/Settings/CreditsAndBilling.tsx index cb6218f9..a4444fa9 100644 --- a/frontend/src/pages/Settings/CreditsAndBilling.tsx +++ b/frontend/src/pages/Settings/CreditsAndBilling.tsx @@ -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 = () => { sum + log.credits_used, 0)} - icon={ClockIcon} + icon={TimeIcon} color="purple" iconColor="text-purple-500" />