diff --git a/backend/igny8_core/business/automation/services/automation_service.py b/backend/igny8_core/business/automation/services/automation_service.py index 1466e51c..87923471 100644 --- a/backend/igny8_core/business/automation/services/automation_service.py +++ b/backend/igny8_core/business/automation/services/automation_service.py @@ -1472,13 +1472,13 @@ class AutomationService: time.sleep(delay) def run_stage_7(self): - """Stage 7: Auto-Approve and Publish Review Content + """Stage 7: Auto-Approve Review Content This stage automatically approves content in 'review' status and - marks it as 'published' (or queues for WordPress sync). + marks it as 'approved' (ready for publishing to WordPress). """ stage_number = 7 - stage_name = "Review → Published" + stage_name = "Review → Approved" start_time = time.time() # Query content ready for review @@ -1538,7 +1538,7 @@ class AutomationService: 'review_total': total_count, 'approved_count': approved_count, 'content_ids': list(Content.objects.filter( - site=self.site, status='published', updated_at__gte=self.run.started_at + site=self.site, status='approved', updated_at__gte=self.run.started_at ).values_list('id', flat=True)), 'partial': True, 'stopped_reason': reason, @@ -1553,8 +1553,8 @@ class AutomationService: stage_number, f"Approving content {idx}/{total_count}: {content.title}" ) - # Approve content by changing status to 'published' - content.status = 'published' + # Approve content by changing status to 'approved' (ready for publishing) + content.status = 'approved' content.save(update_fields=['status', 'updated_at']) approved_count += 1 @@ -1593,7 +1593,7 @@ class AutomationService: time_elapsed = self._format_time_elapsed(start_time) content_ids = list(Content.objects.filter( site=self.site, - status='published', + status='approved', updated_at__gte=self.run.started_at ).values_list('id', flat=True)) @@ -1617,7 +1617,7 @@ class AutomationService: # Release lock cache.delete(f'automation_lock_{self.site.id}') - logger.info(f"[AutomationService] Stage 7 complete: {approved_count} content pieces approved and published") + logger.info(f"[AutomationService] Stage 7 complete: {approved_count} content pieces approved (ready for publishing)") def pause_automation(self): """Pause current automation run""" diff --git a/backend/igny8_core/business/content/models.py b/backend/igny8_core/business/content/models.py index 5e7f7085..8597d436 100644 --- a/backend/igny8_core/business/content/models.py +++ b/backend/igny8_core/business/content/models.py @@ -271,7 +271,8 @@ class Content(SoftDeletableModel, SiteSectorBaseModel): STATUS_CHOICES = [ ('draft', 'Draft'), ('review', 'Review'), - ('published', 'Published'), + ('approved', 'Approved'), # Ready for publishing to external site + ('published', 'Published'), # Actually published on external site ] status = models.CharField( max_length=50, diff --git a/backend/igny8_core/modules/writer/migrations/0014_add_approved_status.py b/backend/igny8_core/modules/writer/migrations/0014_add_approved_status.py new file mode 100644 index 00000000..2c9110a1 --- /dev/null +++ b/backend/igny8_core/modules/writer/migrations/0014_add_approved_status.py @@ -0,0 +1,31 @@ +# Generated by Django 5.2.9 on 2026-01-01 06:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('writer', '0013_add_caption_to_images'), + ] + + operations = [ + migrations.CreateModel( + name='ImagePrompts', + fields=[ + ], + options={ + 'verbose_name': 'Image Prompt', + 'verbose_name_plural': 'Image Prompts', + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('writer.images',), + ), + migrations.AlterField( + model_name='content', + name='status', + field=models.CharField(choices=[('draft', 'Draft'), ('review', 'Review'), ('approved', 'Approved'), ('published', 'Published')], db_index=True, default='draft', help_text='Content status', max_length=50), + ), + ] diff --git a/backend/igny8_core/modules/writer/serializers.py b/backend/igny8_core/modules/writer/serializers.py index 756c52b5..6a4853f5 100644 --- a/backend/igny8_core/modules/writer/serializers.py +++ b/backend/igny8_core/modules/writer/serializers.py @@ -11,8 +11,8 @@ class TasksSerializer(serializers.ModelSerializer): """Serializer for Tasks model - Stage 1 refactored""" cluster_name = serializers.SerializerMethodField() sector_name = serializers.SerializerMethodField() - site_id = serializers.IntegerField(write_only=True, required=False) - sector_id = serializers.IntegerField(write_only=True, required=False) + site_id = serializers.IntegerField(read_only=True) + sector_id = serializers.IntegerField(read_only=True) class Meta: model = Tasks @@ -162,8 +162,8 @@ class ContentSerializer(serializers.ModelSerializer): has_image_prompts = serializers.SerializerMethodField() image_status = serializers.SerializerMethodField() has_generated_images = serializers.SerializerMethodField() - site_id = serializers.IntegerField(write_only=True, required=False) - sector_id = serializers.IntegerField(write_only=True, required=False) + site_id = serializers.IntegerField(read_only=True) + sector_id = serializers.IntegerField(read_only=True) class Meta: model = Content @@ -300,8 +300,8 @@ class ContentSerializer(serializers.ModelSerializer): class ContentTaxonomySerializer(serializers.ModelSerializer): """Serializer for ContentTaxonomy model - Stage 1 refactored""" content_count = serializers.SerializerMethodField() - site_id = serializers.IntegerField(write_only=True, required=False) - sector_id = serializers.IntegerField(write_only=True, required=False) + site_id = serializers.IntegerField(read_only=True) + sector_id = serializers.IntegerField(read_only=True) class Meta: model = ContentTaxonomy diff --git a/backend/igny8_core/modules/writer/views.py b/backend/igny8_core/modules/writer/views.py index 169ffd49..7bd27b8a 100644 --- a/backend/igny8_core/modules/writer/views.py +++ b/backend/igny8_core/modules/writer/views.py @@ -751,6 +751,19 @@ class ContentViewSet(SiteSectorModelViewSet): 'source', ] + def get_queryset(self): + """Override to support status__in filtering for multiple statuses""" + queryset = super().get_queryset() + + # Support status__in query param (comma-separated list of statuses) + status_in = self.request.query_params.get('status__in', None) + if status_in: + statuses = [s.strip() for s in status_in.split(',') if s.strip()] + if statuses: + queryset = queryset.filter(status__in=statuses) + + return queryset + def perform_create(self, serializer): """Override to check monthly word limit and set account""" user = getattr(self.request, 'user', None) diff --git a/frontend/src/components/dashboard/CreditAvailabilityWidget.tsx b/frontend/src/components/dashboard/CreditAvailabilityWidget.tsx index 64a4b634..b284fc8f 100644 --- a/frontend/src/components/dashboard/CreditAvailabilityWidget.tsx +++ b/frontend/src/components/dashboard/CreditAvailabilityWidget.tsx @@ -15,6 +15,7 @@ import { interface CreditAvailabilityWidgetProps { availableCredits: number; totalCredits: number; + usedCredits?: number; // Actual credits used this month from API loading?: boolean; } @@ -29,10 +30,17 @@ const OPERATION_COSTS = { export default function CreditAvailabilityWidget({ availableCredits, totalCredits, + usedCredits: usedCreditsFromApi, loading = false }: CreditAvailabilityWidgetProps) { - const usedCredits = totalCredits - availableCredits; - const usagePercent = totalCredits > 0 ? Math.round((usedCredits / totalCredits) * 100) : 0; + // Use actual used credits from API if provided, otherwise calculate + // Note: availableCredits may include purchased credits beyond plan allocation + const usedCredits = usedCreditsFromApi ?? 0; + + // Calculate usage percentage based on plan allocation + // If available > plan, user has extra credits (purchased or carried over) + const usagePercent = totalCredits > 0 ? Math.min(Math.round((usedCredits / totalCredits) * 100), 100) : 0; + const remainingPercent = Math.max(100 - usagePercent, 0); // Calculate available operations const availableOps = Object.entries(OPERATION_COSTS).map(([key, config]) => ({ @@ -72,11 +80,11 @@ export default function CreditAvailabilityWidget({ className={`h-2 rounded-full transition-all ${ usagePercent > 90 ? 'bg-error-500' : usagePercent > 75 ? 'bg-warning-500' : 'bg-success-500' }`} - style={{ width: `${Math.max(100 - usagePercent, 0)}%` }} + style={{ width: `${remainingPercent}%` }} >
- {totalCredits > 0 ? `${usedCredits.toLocaleString()} of ${totalCredits.toLocaleString()} used (${usagePercent}%)` : 'No credits allocated'} + {totalCredits > 0 ? `${usedCredits.toLocaleString()} of ${totalCredits.toLocaleString()} used this month (${usagePercent}%)` : 'No credits allocated'}
diff --git a/frontend/src/components/dashboard/StandardizedModuleWidget.tsx b/frontend/src/components/dashboard/StandardizedModuleWidget.tsx index bf65f2fa..0d727ebf 100644 --- a/frontend/src/components/dashboard/StandardizedModuleWidget.tsx +++ b/frontend/src/components/dashboard/StandardizedModuleWidget.tsx @@ -87,8 +87,8 @@ export default function StandardizedModuleWidget({ ]; // Define Writer pipeline - using correct content structure - // Content has status: draft, review, published - // totalContent = drafts + review + published + // Content has status: draft, review, approved, published + // totalContent = drafts + review + approved + published // Get writer colors from config const writerColors = useMemo(() => getPipelineColors('writer'), []); diff --git a/frontend/src/components/dashboard/WorkflowCompletionWidget.tsx b/frontend/src/components/dashboard/WorkflowCompletionWidget.tsx index 30abbf28..152435c2 100644 --- a/frontend/src/components/dashboard/WorkflowCompletionWidget.tsx +++ b/frontend/src/components/dashboard/WorkflowCompletionWidget.tsx @@ -139,7 +139,7 @@ export default function WorkflowCompletionWidget({ ]; // Define writer items - using "Content Pages" not "Articles" - // Total content = drafts + review + published + // Total content = drafts + review + approved + published const totalContent = writer.contentDrafts + writer.contentReview + writer.contentPublished; const writerItems = [ { label: 'Content Pages', value: totalContent, barColor: `var(${WORKFLOW_COLORS.writer.contentPages})` }, diff --git a/frontend/src/config/pages/approved.config.tsx b/frontend/src/config/pages/approved.config.tsx index c07d5dd6..422c382c 100644 --- a/frontend/src/config/pages/approved.config.tsx +++ b/frontend/src/config/pages/approved.config.tsx @@ -90,6 +90,27 @@ export function createApprovedPageConfig(params: { ), }, + { + key: 'status', + label: 'Status', + sortable: true, + sortField: 'status', + width: '130px', + render: (value: string, row: Content) => { + // Map internal status to user-friendly labels + const statusConfig: Record