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

@@ -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:

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