fixes
This commit is contained in:
@@ -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 []
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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({
|
||||
|
||||
54
backend/igny8_core/business/billing/views.py
Normal file
54
backend/igny8_core/business/billing/views.py
Normal 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
|
||||
})
|
||||
14
backend/igny8_core/modules/billing/admin_urls.py
Normal file
14
backend/igny8_core/modules/billing/admin_urls.py
Normal 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
|
||||
@@ -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'),
|
||||
]
|
||||
|
||||
|
||||
@@ -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})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -92,3 +92,5 @@ export const Modal: React.FC<ModalProps> = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
|
||||
@@ -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} />}
|
||||
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user