diff --git a/backend/igny8_core/modules/writer/views.py b/backend/igny8_core/modules/writer/views.py index 80e2f6f5..c04aee8d 100644 --- a/backend/igny8_core/modules/writer/views.py +++ b/backend/igny8_core/modules/writer/views.py @@ -1518,6 +1518,77 @@ class ContentViewSet(SiteSectorModelViewSet): request=request ) + @action(detail=True, methods=['post'], url_path='reschedule', url_name='reschedule', permission_classes=[IsAuthenticatedAndActive, IsEditorOrAbove]) + def reschedule(self, request, pk=None): + """ + Reschedule failed or published content for republishing. + Updates scheduled_publish_at and sets site_status back to 'scheduled'. + + POST /api/v1/writer/content/{id}/reschedule/ + { + "scheduled_at": "2026-01-20T14:00:00Z" // ISO 8601 datetime + } + """ + from django.utils import timezone + from dateutil import parser + import logging + + logger = logging.getLogger(__name__) + content = self.get_object() + + # Get scheduled time from request + scheduled_at_str = request.data.get('scheduled_at') + if not scheduled_at_str: + return error_response( + error='scheduled_at is required (ISO 8601 datetime)', + status_code=status.HTTP_400_BAD_REQUEST, + request=request + ) + + # Parse datetime + try: + scheduled_at = parser.isoparse(scheduled_at_str) + if scheduled_at.tzinfo is None: + scheduled_at = timezone.make_aware(scheduled_at) + except (ValueError, TypeError) as e: + return error_response( + error=f'Invalid datetime format: {str(e)}. Use ISO 8601 format.', + status_code=status.HTTP_400_BAD_REQUEST, + request=request + ) + + # Ensure datetime is in the future + if scheduled_at <= timezone.now(): + return error_response( + error='Scheduled time must be in the future', + status_code=status.HTTP_400_BAD_REQUEST, + request=request + ) + + # Store old values for logging + old_status = content.site_status + old_scheduled_at = content.scheduled_publish_at + + # Update content - allow rescheduling from any state + content.site_status = 'scheduled' + content.scheduled_publish_at = scheduled_at + content.site_status_updated_at = timezone.now() + content.save(update_fields=['site_status', 'scheduled_publish_at', 'site_status_updated_at', 'updated_at']) + + logger.info(f"[ContentViewSet.reschedule] Content {content.id} rescheduled from {old_status} (was {old_scheduled_at}) to {scheduled_at}") + + return success_response( + data={ + 'content_id': content.id, + 'site_status': content.site_status, + 'scheduled_publish_at': content.scheduled_publish_at.isoformat(), + 'previous_status': old_status, + 'was_scheduled_for': old_scheduled_at.isoformat() if old_scheduled_at else None, + }, + message=f'Content rescheduled for {scheduled_at.strftime("%Y-%m-%d %H:%M")}', + request=request + ) + @action(detail=False, methods=['post'], url_path='generate_image_prompts', url_name='generate_image_prompts') def generate_image_prompts(self, request): """Generate image prompts for content records - same pattern as other AI functions""" diff --git a/backend/igny8_core/tasks/publishing_scheduler.py b/backend/igny8_core/tasks/publishing_scheduler.py index 37e7fdd1..2e7b541c 100644 --- a/backend/igny8_core/tasks/publishing_scheduler.py +++ b/backend/igny8_core/tasks/publishing_scheduler.py @@ -232,14 +232,15 @@ def process_scheduled_publications() -> Dict[str, Any]: - site_status = 'scheduled' - scheduled_publish_at <= now - For each, triggers the WordPress publishing task. + For each, triggers publishing via PublisherService (current system). + + UPDATED: Uses Site.wp_api_key directly (no SiteIntegration needed). Returns: Dict with processing results """ from igny8_core.business.content.models import Content - from igny8_core.business.integration.models import SiteIntegration - from igny8_core.tasks.wordpress_publishing import publish_content_to_wordpress + from igny8_core.business.publishing.services.publisher_service import PublisherService results = { 'processed': 0, @@ -249,33 +250,25 @@ def process_scheduled_publications() -> Dict[str, Any]: } now = timezone.now() + publisher_service = PublisherService() try: # Get all scheduled content that's due due_content = Content.objects.filter( site_status='scheduled', scheduled_publish_at__lte=now - ).select_related('site', 'sector', 'cluster') + ).select_related('site', 'sector', 'cluster', 'account') + + logger.info(f"[process_scheduled_publications] Found {due_content.count()} content items due for publishing") for content in due_content: results['processed'] += 1 try: - # Update status to publishing - content.site_status = 'publishing' - content.site_status_updated_at = timezone.now() - content.save(update_fields=['site_status', 'site_status_updated_at']) - - # Get site integration - site_integration = SiteIntegration.objects.filter( - site=content.site, - platform='wordpress', - is_active=True - ).first() - - if not site_integration: - error_msg = f"No active WordPress integration for site {content.site_id}" - logger.error(error_msg) + # Validate prerequisites + if not content.site: + error_msg = f"Content {content.id} has no site assigned" + logger.error(f"[process_scheduled_publications] {error_msg}") content.site_status = 'failed' content.site_status_updated_at = timezone.now() content.save(update_fields=['site_status', 'site_status_updated_at']) @@ -283,30 +276,67 @@ def process_scheduled_publications() -> Dict[str, Any]: results['errors'].append(error_msg) continue - # Queue the WordPress publishing task - publish_content_to_wordpress.delay( + # Check WordPress configuration on Site + if not content.site.wp_api_key: + error_msg = f"Site '{content.site.name}' (ID: {content.site.id}) has no WordPress API key configured" + logger.error(f"[process_scheduled_publications] {error_msg}") + content.site_status = 'failed' + content.site_status_updated_at = timezone.now() + content.save(update_fields=['site_status', 'site_status_updated_at']) + results['failed'] += 1 + results['errors'].append(error_msg) + continue + + if not content.site.domain: + error_msg = f"Site '{content.site.name}' (ID: {content.site.id}) has no domain configured" + logger.error(f"[process_scheduled_publications] {error_msg}") + content.site_status = 'failed' + content.site_status_updated_at = timezone.now() + content.save(update_fields=['site_status', 'site_status_updated_at']) + results['failed'] += 1 + results['errors'].append(error_msg) + continue + + # Update status to publishing + content.site_status = 'publishing' + content.site_status_updated_at = timezone.now() + content.save(update_fields=['site_status', 'site_status_updated_at']) + + # Publish via PublisherService (current system) + logger.info(f"[process_scheduled_publications] Publishing content {content.id} '{content.title}' to {content.site.domain}") + publish_result = publisher_service.publish_content( content_id=content.id, - site_integration_id=site_integration.id + destinations=['wordpress'], + account=content.account ) - logger.info(f"Queued content {content.id} for WordPress publishing") - results['published'] += 1 + if publish_result.get('success'): + logger.info(f"[process_scheduled_publications] ✅ Successfully published content {content.id}") + results['published'] += 1 + else: + error_msg = f"Publishing failed for content {content.id}: {publish_result.get('error', 'Unknown error')}" + logger.error(f"[process_scheduled_publications] ❌ {error_msg}") + content.site_status = 'failed' + content.site_status_updated_at = timezone.now() + content.save(update_fields=['site_status', 'site_status_updated_at']) + results['failed'] += 1 + results['errors'].append(error_msg) except Exception as e: error_msg = f"Error processing content {content.id}: {str(e)}" - logger.error(error_msg) + logger.error(f"[process_scheduled_publications] ❌ {error_msg}", exc_info=True) content.site_status = 'failed' content.site_status_updated_at = timezone.now() content.save(update_fields=['site_status', 'site_status_updated_at']) results['failed'] += 1 results['errors'].append(error_msg) - logger.info(f"Processing completed: {results['published']}/{results['processed']} published successfully") + logger.info(f"[process_scheduled_publications] ✅ Completed: {results['published']}/{results['processed']} published successfully, {results['failed']} failed") return results except Exception as e: error_msg = f"Fatal error in process_scheduled_publications: {str(e)}" - logger.error(error_msg) + logger.error(f"[process_scheduled_publications] ❌ {error_msg}", exc_info=True) results['errors'].append(error_msg) return results diff --git a/docs/40-WORKFLOWS/SCHEDULED-CONTENT-PUBLISHING.md b/docs/40-WORKFLOWS/SCHEDULED-CONTENT-PUBLISHING.md index 866e659f..df016eae 100644 --- a/docs/40-WORKFLOWS/SCHEDULED-CONTENT-PUBLISHING.md +++ b/docs/40-WORKFLOWS/SCHEDULED-CONTENT-PUBLISHING.md @@ -1,6 +1,6 @@ # Scheduled Content Publishing Workflow -**Last Updated:** January 12, 2026 +**Last Updated:** January 16, 2026 **Module:** Publishing / Automation --- @@ -9,6 +9,12 @@ IGNY8 provides automated content publishing to WordPress sites. Content goes through a scheduling process before being published at the designated time. +**Current System (v2.0):** +- WordPress credentials stored directly on `Site` model (`wp_api_key`, `domain`) +- No `SiteIntegration` model required +- Publishing via `PublisherService` (not legacy Celery tasks) +- API endpoint: `POST /api/v1/publisher/publish` + --- ## Content Lifecycle for Publishing @@ -115,34 +121,38 @@ Content has **TWO separate status fields**: - `site_status='scheduled'` - `scheduled_publish_at <= now` 2. For each content item: + - Validates WordPress configuration exists on Site (`wp_api_key`, `domain`) - Updates `site_status='publishing'` - - Gets the site's WordPress integration - - Queues `publish_content_to_wordpress` Celery task + - Calls `PublisherService.publish_content()` directly (synchronous) + - Updates `site_status='published'` on success or `'failed'` on error 3. Logs results and any errors +**Current Implementation (v2.0):** +- Uses `PublisherService` (current system) +- Gets config from `Site.wp_api_key` and `Site.domain` +- No `SiteIntegration` required +- Synchronous publishing (not queued to Celery) + --- -### 3. `publish_content_to_wordpress` +### 3. `PublisherService.publish_content` -**Type:** On-demand Celery task (queued by `process_scheduled_publications`) -**Task Name:** `publishing.publish_content_to_wordpress` -**File:** `backend/igny8_core/tasks/wordpress_publishing.py` +**Type:** Service method (called by scheduler) +**File:** `backend/igny8_core/business/publishing/services/publisher_service.py` #### What It Does: -1. **Load Content & Integration** - Gets content and WordPress credentials -2. **Check Already Published** - Skips if `external_id` exists -3. **Generate Excerpt** - Creates excerpt from HTML content -4. **Get Taxonomy Terms** - Loads categories and tags from `ContentTaxonomy` -5. **Get Images** - Loads featured image and gallery images -6. **Build API Payload** - Constructs WordPress REST API payload -7. **Call WordPress API** - POSTs to WordPress via IGNY8 Bridge plugin -8. **Update Content** - Sets `external_id`, `external_url`, `site_status='published'` -9. **Log Sync Event** - Records in `SyncEvent` model +1. **Load Content** - Gets content by ID +2. **Get WordPress Config** - Reads from `Site.wp_api_key` and `Site.domain` +3. **Call Adapter** - Uses `WordPressAdapter` to handle API communication +4. **WordPress API Call** - POSTs to `{domain}/wp-json/igny8/v1/publish` +5. **Update Content** - Sets `external_id`, `external_url`, `site_status='published'` +6. **Create Record** - Logs in `PublishingRecord` model #### WordPress Connection: - Uses the IGNY8 WordPress Bridge plugin installed on the site -- API endpoint: `{site_url}/wp-json/igny8-bridge/v1/publish` -- Authentication: API key stored in `Site.wp_api_key` +- API endpoint: `{site.domain}/wp-json/igny8/v1/publish` +- Authentication: API key from `Site.wp_api_key` +- **No SiteIntegration model needed** --- @@ -201,15 +211,39 @@ app.conf.beat_schedule = { ## Manual Publishing -Content can also be published immediately via: +Content can be published immediately or rescheduled via API: -### API Endpoint +### Publish Now ``` -POST /api/v1/content/{content_id}/publish/ +POST /api/v1/publisher/publish +{ + "content_id": 123, + "destinations": ["wordpress"] +} ``` -### Admin Action -In Django Admin, select content and use "Publish to WordPress" action. +### Schedule for Later +``` +POST /api/v1/writer/content/{id}/schedule/ +{ + "scheduled_at": "2026-01-20T14:00:00Z" +} +``` + +### Reschedule Failed Content +``` +POST /api/v1/writer/content/{id}/reschedule/ +{ + "scheduled_at": "2026-01-20T15:00:00Z" +} +``` + +**Note:** Reschedule works from any `site_status` (failed, published, scheduled, etc.) + +### Unschedule +``` +POST /api/v1/writer/content/{id}/unschedule/ +``` --- @@ -267,16 +301,40 @@ process_scheduled_publications() | Error | Cause | Solution | |-------|-------|----------| -| No active WordPress integration | Site doesn't have WordPress connected | Configure integration in Site settings | +| No WordPress API key | Site.wp_api_key is empty | Configure API key in Site settings | +| No domain configured | Site.domain is empty | Set domain in Site settings | | API key invalid/expired | WordPress API key issue | Regenerate API key in WordPress plugin | | Connection timeout | WordPress site unreachable | Check site availability | | Plugin not active | IGNY8 Bridge plugin disabled | Enable plugin in WordPress | -| Content already published | Duplicate publish attempt | Check `external_id` field | +| Content already published | Duplicate publish attempt | Use reschedule to republish | ### Retry Policy -- `publish_content_to_wordpress` has `max_retries=3` -- Automatic retry on transient failures - Failed content marked with `site_status='failed'` +- Use the `reschedule` action to retry publishing +- Can reschedule from any status (failed, published, etc.) + +### Rescheduling Failed Content + +```python +# Via API +POST /api/v1/writer/content/{id}/reschedule/ +{ + "scheduled_at": "2026-01-20T15:00:00Z" +} + +# Via Django shell +from igny8_core.business.content.models import Content +from django.utils import timezone +from datetime import timedelta + +failed_content = Content.objects.filter(site_status='failed') +for content in failed_content: + # Reschedule for 1 hour from now + content.site_status = 'scheduled' + content.scheduled_publish_at = timezone.now() + timedelta(hours=1) + content.site_status_updated_at = timezone.now() + content.save(update_fields=['site_status', 'scheduled_publish_at', 'site_status_updated_at']) +``` --- @@ -294,19 +352,37 @@ process_scheduled_publications() 1. Check Celery Beat is running: `docker compose logs igny8_celery_beat` 2. Check Celery Worker is running: `docker compose logs igny8_celery_worker` 3. Look for errors in worker logs -4. Verify WordPress integration is active -5. Test WordPress API connectivity +4. Verify Site has `wp_api_key` configured +5. Verify Site has `domain` configured +6. Test WordPress API connectivity ### Resetting Failed Content -```python -# Reset failed content to try again -from igny8_core.business.content.models import Content +Use the reschedule API endpoint: -Content.objects.filter(site_status='failed').update( - site_status='not_published', - scheduled_publish_at=None -) +```bash +# Reschedule single content +curl -X POST https://api.igny8.com/api/v1/writer/content/344/reschedule/ \ + -H "Authorization: Api-Key YOUR_KEY" \ + -H "Content-Type: application/json" \ + -d '{"scheduled_at": "2026-01-20T15:00:00Z"}' +``` + +Or via Django shell: + +```python +# Reset all failed content to reschedule in 1 hour +from igny8_core.business.content.models import Content +from django.utils import timezone +from datetime import timedelta + +failed_content = Content.objects.filter(site_status='failed') +for content in failed_content: + content.site_status = 'scheduled' + content.scheduled_publish_at = timezone.now() + timedelta(hours=1) + content.site_status_updated_at = timezone.now() + content.save(update_fields=['site_status', 'scheduled_publish_at', 'site_status_updated_at']) + print(f"Rescheduled content {content.id}") ``` --- @@ -318,29 +394,12 @@ Content.objects.filter(site_status='failed').update( │ IGNY8 Backend │ ├─────────────────────────────────────────────────────────────────┤ │ │ -│ ┌──────────────────┐ ┌──────────────────┐ │ -│ │ Celery Beat │ │ Celery Worker │ │ -│ │ │ │ │ │ -│ │ Sends tasks at │───▶│ Executes tasks │ │ -│ │ scheduled times │ │ │ │ -│ └──────────────────┘ └────────┬─────────┘ │ -│ │ │ -│ ▼ │ -│ ┌────────────────────────────────────────────────────────┐ │ -│ │ Publishing Tasks │ │ +│ ┌───────Validate Site.wp_api_key & domain │ │ +│ │ - Call PublisherService.publish_content() │ │ │ │ │ │ -│ │ 1. schedule_approved_content (hourly) │ │ -│ │ - Find approved content │ │ -│ │ - Calculate publish slots │ │ -│ │ - Set scheduled_publish_at │ │ -│ │ │ │ -│ │ 2. process_scheduled_publications (every 5 min) │ │ -│ │ - Find due content │ │ -│ │ - Queue publish_content_to_wordpress │ │ -│ │ │ │ -│ │ 3. publish_content_to_wordpress │ │ -│ │ - Build API payload │ │ -│ │ - Call WordPress REST API │ │ +│ │ 3. PublisherService │ │ +│ │ - Get config from Site model │ │ +│ │ - Call WordPressAdapter │ │ │ │ - Update content status │ │ │ └────────────────────────────────────────────────────────┘ │ │ │ │ @@ -353,6 +412,68 @@ Content.objects.filter(site_status='failed').update( │ ┌─────────────────────────────────────────────────────────┐ │ │ │ IGNY8 Bridge Plugin │ │ │ │ │ │ +│ │ /wp-json/igny8/v1/publish │ │ +│ │ - Receives content payload │ │ +│ │ - Creates/updates WordPress post │ │ +│ │ - Handles images, categories, tags │ │ +│ │ - Returns post ID and URL │ │ +│ └─────────────────────────────────────────────────────────┘ │ +└───────────────────────────────────────────────────────────────┘ +``` + +--- + +## API Reference + +### Schedule Content +```http +POST /api/v1/writer/content/{id}/schedule/ +Authorization: Api-Key YOUR_KEY +Content-Type: application/json + +{ + "scheduled_at": "2026-01-20T14:00:00Z" +} +``` + +### Reschedule Content (Failed or Any Status) +```http +POST /api/v1/writer/content/{id}/reschedule/ +Authorization: Api-Key YOUR_KEY +Content-Type: application/json + +{ + "scheduled_at": "2026-01-20T15:00:00Z" +} +``` + +### Unschedule Content +```http +POST /api/v1/writer/content/{id}/unschedule/ +Authorization: Api-Key YOUR_KEY +``` + +### Publish Immediately +```http +POST /api/v1/publisher/publish +Authorization: Api-Key YOUR_KEY +Content-Type: application/json + +{ + "content_id": 123, + "destinations": ["wordpress"] +}│ +│ └────────────────────────────────────────────────────────┘ │ +│ │ │ +└───────────────────────────────────┼─────────────────────────────┘ + │ + ▼ HTTPS +┌───────────────────────────────────────────────────────────────┐ +│ WordPress Site │ +├───────────────────────────────────────────────────────────────┤ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ IGNY8 Bridge Plugin │ │ +│ │ │ │ │ │ /wp-json/igny8-bridge/v1/publish │ │ │ │ - Receives content payload │ │ │ │ - Creates/updates WordPress post │ │ diff --git a/docs/plans/PUBLISHING-PROGRESS-AND-SCHEDULING-UX-PLAN.md b/docs/plans/PUBLISHING-PROGRESS-AND-SCHEDULING-UX-PLAN.md new file mode 100644 index 00000000..f93f1f91 --- /dev/null +++ b/docs/plans/PUBLISHING-PROGRESS-AND-SCHEDULING-UX-PLAN.md @@ -0,0 +1,1595 @@ +# Publishing Progress & Scheduling UX Enhancement Plan + +## Document Overview + +**Author**: System Analysis +**Date**: January 2025 +**Status**: Planning Phase +**Target**: Publishing UX improvements with progress tracking and scheduling interfaces for multi-platform publishing (WordPress, Shopify, Custom Sites) + +--- + +## 1. Executive Summary + +This document provides a comprehensive implementation plan for enhancing the publishing workflow with: + +1. **Publishing Progress Modals** - Real-time feedback for single and bulk publishing operations +2. **Publishing Limits & Validation** - Direct bulk publish limited to 5 items, unlimited scheduling +3. **Workflow Optimization** - Remove publish from Review page, make it Approved-only +4. **Scheduling UI Enhancement** - Add scheduling/rescheduling interfaces across multiple pages +5. **Site Settings Integration** - Bulk scheduling uses default settings from Site Settings → Publishing tab +6. **Failed Content Handling** - UI for rescheduling failed scheduled publications + +### Key Design Principles + +- **Pattern Consistency**: Follow existing modal patterns from ImageQueueModal and content generation +- **Progressive Disclosure**: Show appropriate details based on operation complexity +- **Real-time Feedback**: Live progress updates during publishing operations +- **Error Recovery**: Clear paths to retry/reschedule failed operations + +--- + +## 2. Current System Analysis + +### 2.1 Existing Publishing Architecture + +**Backend System (v2.0)**: +- Service: `PublisherService` at `backend/igny8_core/business/publishing/services/publisher_service.py` +- API Endpoint: `POST /api/v1/publisher/publish` +- Credentials: Stored on `Site` model (`api_key`, `domain`, `platform_type`) +- Supported Platforms: WordPress, Shopify, Custom Sites +- Processing: Synchronous (not queued to Celery) +- Site Settings: Default publishing/scheduling configuration at `/sites/{site_id}/settings?tab=publishing` + +**Scheduling System**: +- Celery Beat Task: `process_scheduled_publications` (runs every 5 minutes) +- Content States: + - `site_status`: `not_published` → `scheduled` → `publishing` → `published`/`failed` + - `scheduled_publish_at`: ISO8601 datetime + - `site_status_updated_at`: Timestamp of last status change +- API Endpoints: + - `POST /api/v1/writer/content/{id}/schedule/` - Schedule for future publishing + - `POST /api/v1/writer/content/{id}/unschedule/` - Cancel scheduled publishing + - `POST /api/v1/writer/content/{id}/reschedule/` - Reschedule from any status (NEW) + +### 2.2 Current Publishing Workflows + +**Review Page** (`frontend/src/pages/Writer/Review.tsx`): +- **Current Behavior**: Shows content with `status='review'` +- **Actions Available**: + - Single publish: `handlePublishSingle()` → calls `POST /v1/publisher/publish/` + - Bulk publish: `handlePublishBulk()` → loops through items, publishes individually + - Approve (changes status to 'approved') +- **Issue**: Publishing from Review bypasses approval workflow +- **No Feedback**: No progress modal, only toast notifications +- **Current Issue**: Uses "WordPress" terminology, limiting perceived platform support + +**Approved Page** (`frontend/src/pages/Writer/Approved.tsx`): +- **Current Behavior**: Shows content with `status='approved'` or `status='published'` +- **Actions Available**: + - Single publish: `handleRowAction('publish_site')` → `POST /v1/publisher/publish/` (no limit) + - Bulk publish: `handleBulkPublishToSite()` → loops through items (max 5 items) + - Bulk schedule: Uses site default settings (no limit on items) +- **No Feedback**: No progress modal, only toast notifications +- **No Scheduling**: Cannot schedule for future publishing from UI +- **No Validation**: No limit enforcement for bulk operations +- **Current Issue**: Action names reference specific platforms instead of generic "site" terminology + +**Content Calendar** (`frontend/src/pages/Publisher/ContentCalendar.tsx`): +- **Current Behavior**: Shows scheduled items with calendar view +- **Scheduling Method**: Drag-and-drop from approved sidebar to calendar dates +- **Schedule Time**: Defaults to 9 AM on dropped date +- **API Calls**: + - `scheduleContent(contentId, scheduledDate)` → `POST /v1/writer/content/{id}/schedule/` + - `unscheduleContent(contentId)` → `POST /v1/writer/content/{id}/unschedule/` +- **No Edit UI**: Cannot edit scheduled time without drag-drop + +### 2.3 Existing Modal Patterns + +**ImageQueueModal** (`frontend/src/components/common/ImageQueueModal.tsx`): +- **Purpose**: Shows AI image generation progress for multiple images +- **Key Features**: + - Queue display with individual progress bars + - Status per item: `pending` → `processing` → `completed`/`failed` + - Smooth progress animation (1% → 50% in 5s, 50% → 80% at 2%/200ms, 80% → 100% on completion) + - Real-time polling: `GET /v1/system/settings/task_progress/{taskId}/` every 1 second + - Visual feedback: Status icons, color-coded progress bars + - Thumbnail preview on completion + - Cannot close while processing +- **State Management**: + ```typescript + interface ImageQueueItem { + imageId: number | null; + index: number; + label: string; + status: 'pending' | 'processing' | 'completed' | 'failed'; + progress: number; + imageUrl: string | null; + error: string | null; + } + ``` +- **Progress Tracking**: + - Backend provides: `current_image`, `current_image_id`, `current_image_progress`, `results[]` + - Frontend manages smooth animation with `smoothProgress` state + - Progress intervals cleared on completion/failure + +--- + +## 3. Publishing Progress Modal Implementation + +### 3.1 Single Content Publishing Modal + +**Component**: `PublishingProgressModal` (new) +**Location**: `frontend/src/components/common/PublishingProgressModal.tsx` + +#### Design Specifications + +**Modal Structure**: +``` +┌────────────────────────────────────────────────────┐ +│ 🚀 Publishing Content │ +│ Publishing "Article Title Here" to [Site Name] │ +├────────────────────────────────────────────────────┤ +│ │ +│ ┌────────────────────────────────────────────────┐ │ +│ │ 📄 Preparing content... 25% │ │ +│ │ ████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ │ +│ └────────────────────────────────────────────────┘ │ +│ │ +│ Status: Uploading to WordPress... │ +│ │ +├────────────────────────────────────────────────────┤ +│ [Close button - only shown on completion/error] │ +└────────────────────────────────────────────────────┘ +``` + +**Progress Stages**: +1. **Preparing content** (0-25%): Validating content structure +2. **Uploading to site** (25-50%): POST request to publishing platform API +3. **Processing response** (50-75%): Handling platform API response +4. **Finalizing** (75-100%): Updating content record + +**State Interface**: +```typescript +interface PublishingProgressState { + contentId: number; + contentTitle: string; + destination: 'wordpress' | 'shopify' | 'custom' | string; // Platform type + siteName: string; // Actual site name for display + status: 'preparing' | 'uploading' | 'processing' | 'finalizing' | 'completed' | 'failed'; + progress: number; // 0-100 + statusMessage: string; + error: string | null; + externalUrl: string | null; // Published URL on success + externalId: string | null; // External platform post/page ID +} +``` + +**Progress Animation**: +- **Phase 1** (0-25%): 1% every 100ms for 2.5 seconds +- **Phase 2** (25-50%): During actual API call (may vary) +- **Phase 3** (50-75%): 2% every 80ms for 1 second +- **Phase 4** (75-100%): Fast animation on success (800ms) + +**Error Handling**: +- Show error message in red alert box +- Display "Retry" and "Close" buttons +- Log full error for debugging +- Option to copy error details + +**Success State**: +- Green checkmark icon +- "View on Site" button (opens `external_url` in new tab) +- Shows actual site name: "View on [Site Name]" +- "Close" button +- Auto-close after 3 seconds (optional) + +#### API Integration + +**Endpoint**: `POST /api/v1/publisher/publish/` +**Request**: +```json +{ + "content_id": 123, + "destinations": ["wordpress"] // Can be: "wordpress", "shopify", "custom" +} +``` + +**Response** (success): +```json +{ + "success": true, + "data": { + "success": true, + "results": [ + { + "destination": "wordpress", // or "shopify", "custom" + "success": true, + "external_id": "456", + "url": "https://site.com/article-title/", + "publishing_record_id": 789, + "platform_type": "wordpress" + } + ] + } +} +``` + +**Response** (failure): +```json +{ + "success": false, + "error": "Publishing API error: Invalid credentials" +} +``` + +**Since publishing is synchronous**, the modal flow: +1. Open modal immediately on "Publish" click +2. Show "Preparing" stage with progress 0-25% +3. Make API call (update to "Uploading" at 25%) +4. On response: + - Success: Animate 25% → 100% with status updates + - Failure: Show error state +5. Update parent component's content list + +### 3.2 Bulk Publishing Modal + +**Component**: `BulkPublishingModal` (new) +**Location**: `frontend/src/components/common/BulkPublishingModal.tsx` + +#### Design Specifications + +**Modal Structure** (similar to ImageQueueModal): +``` +┌──────────────────────────────────────────────────────────┐ +│ 🚀 Publishing Content │ +│ Publishing 5 articles to [Site Name] │ +├──────────────────────────────────────────────────────────┤ +│ │ +│ ┌────────────────────────────────────────────────────┐ │ +│ │ 1️⃣ First Article Title ✓ 100%│ │ +│ │ █████████████████████████████████████████████████ │ │ +│ │ Published: https://site.com/article-1/ │ │ +│ └────────────────────────────────────────────────────┘ │ +│ │ +│ ┌────────────────────────────────────────────────────┐ │ +│ │ 2️⃣ Second Article Title ⏳ 45% │ │ +│ │ ████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ │ +│ │ Status: Uploading to site... │ │ +│ └────────────────────────────────────────────────────┘ │ +│ │ +│ ┌────────────────────────────────────────────────────┐ │ +│ │ 3️⃣ Third Article Title ⏸️ 0% │ │ +│ │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ │ +│ │ Status: Pending │ │ +│ └────────────────────────────────────────────────────┘ │ +│ │ +│ [2 more items...] │ +│ │ +├──────────────────────────────────────────────────────────┤ +│ 1 completed, 1 failed, 3 pending │ +│ [Close - if done] │ +└──────────────────────────────────────────────────────────┘ +``` + +**Queue Item State**: +```typescript +interface PublishQueueItem { + contentId: number; + contentTitle: string; + index: number; + status: 'pending' | 'processing' | 'completed' | 'failed'; + progress: number; // 0-100 + statusMessage: string; + error: string | null; + externalUrl: string | null; + externalId: string | null; +} +``` + +**Processing Strategy**: +- **Sequential Processing**: Publish one item at a time (not parallel) +- **Reason**: Avoid overwhelming platform APIs, easier error tracking, respects rate limits +- **Progress Animation**: Same as single modal per item +- **Status Updates**: Real-time per item +- **Platform Aware**: Adapts to WordPress, Shopify, or custom platform requirements + +**Visual Indicators**: +- ✓ (green) - Completed +- ⏳ (blue) - Processing +- ⏸️ (gray) - Pending +- ❌ (red) - Failed + +**Error Recovery**: +- Failed items show "Retry" button +- Can retry individual failed items +- Can skip failed items and continue +- Summary at bottom: X completed, Y failed, Z pending + +**Cannot Close Until Complete**: +- Close button disabled while `status='processing'` items exist +- User must wait for all items to complete/fail +- Similar to ImageQueueModal behavior + +#### Implementation Flow + +```typescript +const handleBulkPublish = async (contentIds: number[]) => { + // Initialize queue + const queue: PublishQueueItem[] = contentIds.map((id, index) => ({ + contentId: id, + contentTitle: getContentTitle(id), // Lookup from parent + index: index + 1, + status: 'pending', + progress: 0, + statusMessage: 'Pending', + error: null, + externalUrl: null, + externalId: null, + })); + + // Open modal with queue + setPublishQueue(queue); + setIsModalOpen(true); + + // Process sequentially + for (let i = 0; i < queue.length; i++) { + // Update status to processing + updateQueueItem(i, { status: 'processing', progress: 0, statusMessage: 'Preparing...' }); + + try { + // Simulate progress animation + animateProgress(i, 0, 25, 2500); // 0-25% in 2.5s + + // Call API (destination determined by site's platform_type) + const response = await fetchAPI('/v1/publisher/publish/', { + method: 'POST', + body: JSON.stringify({ + content_id: queue[i].contentId, + destinations: [sitePlatformType] // 'wordpress', 'shopify', or 'custom' + }) + }); + + // Handle response + if (response.success && response.data?.results?.[0]?.success) { + const result = response.data.results[0]; + animateProgress(i, 25, 100, 1500); // 25-100% in 1.5s + updateQueueItem(i, { + status: 'completed', + progress: 100, + statusMessage: 'Published', + externalUrl: result.url, + externalId: result.external_id, + }); + } else { + updateQueueItem(i, { + status: 'failed', + progress: 0, + statusMessage: 'Failed', + error: response.error || 'Unknown error', + }); + } + } catch (error) { + updateQueueItem(i, { + status: 'failed', + progress: 0, + statusMessage: 'Failed', + error: error.message, + }); + } + } + + // All done - enable close button + setAllComplete(true); +}; +``` + +### 3.3 Publishing Limits & Validation + +#### Direct Publish Limit (5 Items Max) + +**Rationale**: +- Direct publishing is synchronous and resource-intensive +- Limits server load and prevents API rate limiting +- Encourages use of scheduling for large batches +- Single-item publish (3-dot menu) has no limit (only 1 item) + +**Implementation**: + +**Validation in Approved Page**: +```typescript +const handleBulkPublishClick = () => { + const selectedCount = selectedIds.length; + + // Validate: Max 5 items for direct bulk publish + if (selectedCount > 5) { + // Show limit exceeded modal + setShowPublishLimitModal(true); + return; + } + + // Proceed with bulk publish + handleBulkPublishToSite(selectedIds); +}; +``` + +**Limit Exceeded Modal**: +``` +┌────────────────────────────────────────────────────────────┐ +│ ⚠️ Publishing Limit Exceeded │ +├────────────────────────────────────────────────────────────┤ +│ You can publish only 5 content pages to site directly. │ +│ │ +│ You have selected {X} items. │ +│ │ +│ Options: │ +│ • Deselect items to publish 5 or fewer │ +│ • Use "Schedule Selected" to schedule all items │ +│ │ +│ ℹ️ Tip: Scheduling has no limit and uses your site's │ +│ default publishing schedule. │ +├────────────────────────────────────────────────────────────┤ +│ [Schedule Selected] [Go Back] │ +└────────────────────────────────────────────────────────────┘ +``` + +**Modal Component**: `PublishLimitModal` (new) +```typescript +interface PublishLimitModalProps { + isOpen: boolean; + onClose: () => void; + selectedCount: number; + onScheduleInstead: () => void; +} + +const PublishLimitModal = ({ isOpen, onClose, selectedCount, onScheduleInstead }) => { + return ( + +
+ +

Publishing Limit Exceeded

+

+ You can publish only 5 content pages to site directly. +

+

+ You have selected {selectedCount} items. +

+ +
+

Options:

+
    +
  • Deselect items to publish 5 or fewer
  • +
  • Use "Schedule Selected" to schedule all items
  • +
+

+ 💡 Tip: Scheduling has no limit and uses your site's default publishing schedule. +

+
+ +
+ + +
+
+
+ ); +}; +``` + +**Button State Management**: +```typescript +// In Approved.tsx +const [showPublishLimitModal, setShowPublishLimitModal] = useState(false); + +// Update primary action button +primaryAction={{ + label: selectedIds.length > 5 ? 'Publish (Limit Exceeded)' : 'Publish to Site', + icon: , + onClick: handleBulkPublishClick, + variant: 'success', + disabled: selectedIds.length === 0, + tooltip: selectedIds.length > 5 + ? 'You can only publish 5 items at once. Use scheduling for more.' + : undefined +}} +``` + +#### Scheduling Has No Limit + +**Bulk Schedule Action**: +- No limit on number of items +- Uses Site Settings default schedule +- Processes items through scheduling queue +- Better for large batches (10+ items) + +**Visual Distinction**: +```typescript +// Bulk actions in Approved page +bulkActions: [ + { + label: 'Publish Now (Max 5)', + value: 'bulk_publish_now', + icon: RocketLaunchIcon, + variant: 'success', + disabled: (selectedIds) => selectedIds.length > 5, + tooltip: (selectedIds) => + selectedIds.length > 5 + ? 'Can only publish 5 items directly. Use scheduling for more.' + : 'Publish selected items immediately' + }, + { + label: 'Schedule Selected', + value: 'bulk_schedule', + icon: CalendarIcon, + variant: 'primary', + tooltip: 'Schedule items using site default settings (no limit)' + } +] +``` + +### 3.4 Site Settings Integration for Bulk Scheduling + +#### Default Publishing Schedule Configuration + +**Location**: `/sites/{site_id}/settings?tab=publishing` + +**Configuration Fields** (already exists in site settings): +- **Auto-publish Schedule**: Time of day to publish (e.g., 9:00 AM) +- **Publishing Frequency**: Daily, every X hours, specific days of week +- **Timezone**: Site's timezone for scheduling +- **Stagger Interval**: Minutes between each publish (e.g., 15 min intervals) +- **Max Daily Publishes**: Limit publications per day (optional) + +**Used By**: +1. Automation system (existing) +2. Bulk scheduling from Approved page (new) +3. Content Calendar default times (new) + +#### Bulk Schedule Implementation + +**API Endpoint**: `POST /api/v1/writer/content/bulk_schedule/` + +**Request**: +```json +{ + "content_ids": [123, 124, 125, 126], + "use_site_defaults": true, + "site_id": 45 +} +``` + +**Response**: +```json +{ + "success": true, + "scheduled_count": 4, + "schedule_preview": [ + { + "content_id": 123, + "scheduled_at": "2025-01-17T09:00:00Z", + "title": "First Article" + }, + { + "content_id": 124, + "scheduled_at": "2025-01-17T09:15:00Z", + "title": "Second Article" + }, + { + "content_id": 125, + "scheduled_at": "2025-01-17T09:30:00Z", + "title": "Third Article" + }, + { + "content_id": 126, + "scheduled_at": "2025-01-17T09:45:00Z", + "title": "Fourth Article" + } + ], + "site_settings": { + "base_time": "09:00 AM", + "stagger_interval": 15, + "timezone": "America/New_York" + } +} +``` + +**Frontend Implementation**: +```typescript +const handleBulkScheduleWithDefaults = async (contentIds: number[]) => { + try { + // Show preview modal first + const preview = await fetchAPI('/v1/writer/content/bulk_schedule_preview/', { + method: 'POST', + body: JSON.stringify({ + content_ids: contentIds, + site_id: activeSite.id + }) + }); + + // Open confirmation modal with schedule preview + setBulkSchedulePreview(preview); + setShowBulkScheduleConfirmModal(true); + } catch (error) { + toast.error(`Failed to generate schedule preview: ${error.message}`); + } +}; + +const confirmBulkSchedule = async () => { + try { + const response = await fetchAPI('/v1/writer/content/bulk_schedule/', { + method: 'POST', + body: JSON.stringify({ + content_ids: selectedIds.map(id => parseInt(id)), + use_site_defaults: true, + site_id: activeSite.id + }) + }); + + toast.success(`Scheduled ${response.scheduled_count} items`); + loadContent(); // Refresh + setShowBulkScheduleConfirmModal(false); + } catch (error) { + toast.error(`Failed to schedule: ${error.message}`); + } +}; +``` + +**Bulk Schedule Preview Modal**: +``` +┌────────────────────────────────────────────────────────────┐ +│ 📅 Schedule 10 Articles │ +├────────────────────────────────────────────────────────────┤ +│ Using site default schedule: │ +│ • Start time: 9:00 AM (America/New_York) │ +│ • Stagger: 15 minutes between each │ +│ • First publish: Tomorrow, Jan 17, 2025 at 9:00 AM │ +│ • Last publish: Tomorrow, Jan 17, 2025 at 11:15 AM │ +│ │ +│ Schedule Preview: │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ 1. First Article → Jan 17, 9:00 AM │ │ +│ │ 2. Second Article → Jan 17, 9:15 AM │ │ +│ │ 3. Third Article → Jan 17, 9:30 AM │ │ +│ │ 4. Fourth Article → Jan 17, 9:45 AM │ │ +│ │ 5. Fifth Article → Jan 17, 10:00 AM │ │ +│ │ ... and 5 more │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ │ +│ ℹ️ Modify defaults at Site Settings → Publishing tab │ +│ │ +├────────────────────────────────────────────────────────────┤ +│ [Change Settings] [Cancel] [Confirm Schedule] │ +└────────────────────────────────────────────────────────────┘ +``` + +**Link to Site Settings**: +```typescript +const handleChangeSettings = () => { + // Open site settings in new tab + window.open(`/sites/${activeSite.id}/settings?tab=publishing`, '_blank'); + // Keep modal open so user can return and confirm +}; +``` + +--- + +## 4. Workflow Changes: Remove Publish from Review Page + +### 4.1 Current Problem + +**Review Page** currently allows direct publishing, which: +- Bypasses the approval workflow +- Allows unapproved content to go live +- Creates confusion about content workflow states +- Inconsistent with expected review → approve → publish flow +- Uses platform-specific terminology ("Publish to WordPress") limiting perceived capabilities + +### 4.2 Proposed Changes + +**Review Page** (`frontend/src/pages/Writer/Review.tsx`): + +**Remove**: +- ❌ `handlePublishSingle()` function +- ❌ `handlePublishBulk()` function +- ❌ "Publish to WordPress" action (replace with generic "Publish" in Approved page) +- ❌ "Publish to Site" bulk action button from Review page + +**Keep**: +- ✅ `handleApproveSingle()` - Approve individual items +- ✅ `handleApproveBulk()` - Approve multiple items +- ✅ "Approve" action (changes status to 'approved') +- ✅ View, Edit, Delete actions + +**New Primary Action**: +```typescript +primaryAction={{ + label: 'Approve', + icon: , + onClick: () => handleBulkAction('bulk_approve', selectedIds), + variant: 'success', +}} +``` + +**Updated Page Description**: +```typescript +// Old: "Shows content with status='review' ready for publishing" +// New: "Shows content with status='review' ready for approval" +``` + +**Impact**: +- Content in Review can only be approved +- To publish, content must first move to Approved page +- Clearer workflow: Draft → Review → Approved → Publish +- No breaking changes to backend + +### 4.3 Updated Row Actions Config + +**File**: `frontend/src/config/pages/review.config.tsx` + +**Before**: +```typescript +rowActions: [ + { label: 'Approve', value: 'approve', icon: CheckCircleIcon }, + { label: 'Publish to WordPress', value: 'publish_wordpress', icon: RocketLaunchIcon }, // Platform-specific + { label: 'View', value: 'view', icon: EyeIcon }, + { label: 'Edit', value: 'edit', icon: PencilIcon }, + { label: 'Delete', value: 'delete', icon: TrashBinIcon, danger: true }, +] +``` + +**After**: +```typescript +rowActions: [ + { label: 'Approve', value: 'approve', icon: CheckCircleIcon }, + { label: 'View', value: 'view', icon: EyeIcon }, + { label: 'Edit', value: 'edit', icon: PencilIcon }, + { label: 'Delete', value: 'delete', icon: TrashBinIcon, danger: true }, +] +``` + +--- + +## 5. Scheduling UI Implementation + +### 5.1 Schedule/Reschedule in Approved Page + +**Component**: Add scheduling button to row actions and bulk actions + +#### Row Action: Schedule Single Item + +**Modal**: `ScheduleContentModal` (new) +**Location**: `frontend/src/components/common/ScheduleContentModal.tsx` + +**Design**: +``` +┌────────────────────────────────────────┐ +│ 📅 Schedule Content Publishing │ +├────────────────────────────────────────┤ +│ Content: "Article Title Here" │ +│ │ +│ Schedule Date: │ +│ [Date Picker: MM/DD/YYYY ▼] │ +│ │ +│ Schedule Time: │ +│ [Time Picker: HH:MM AM/PM ▼] │ +│ │ +│ Preview: January 15, 2025 at 9:00 AM │ +│ │ +├────────────────────────────────────────┤ +│ [Cancel] [Schedule] │ +└────────────────────────────────────────┘ +``` + +**Implementation**: +```typescript +interface ScheduleContentModalProps { + isOpen: boolean; + onClose: () => void; + content: Content; + onSchedule: (contentId: number, scheduledDate: string) => Promise; +} + +const ScheduleContentModal = ({ isOpen, onClose, content, onSchedule }) => { + const [selectedDate, setSelectedDate] = useState(getDefaultScheduleDate()); + const [selectedTime, setSelectedTime] = useState('09:00 AM'); + + const getDefaultScheduleDate = () => { + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + return tomorrow; + }; + + const handleSchedule = async () => { + // Combine date and time + const scheduledDateTime = combineDateAndTime(selectedDate, selectedTime); + + try { + await onSchedule(content.id, scheduledDateTime.toISOString()); + toast.success(`Scheduled for ${formatScheduledTime(scheduledDateTime)}`); + onClose(); + } catch (error) { + toast.error(`Failed to schedule: ${error.message}`); + } + }; + + return ( + + {/* Date/Time pickers */} + + + ); +}; +``` + +**API Call**: +```typescript +const handleScheduleContent = async (contentId: number, scheduledDate: string) => { + const response = await fetchAPI(`/v1/writer/content/${contentId}/schedule/`, { + method: 'POST', + body: JSON.stringify({ scheduled_publish_at: scheduledDate }), + }); + + // Update local state + setContent(prevContent => + prevContent.map(c => c.id === contentId ? { + ...c, + site_status: 'scheduled', + scheduled_publish_at: scheduledDate, + } : c) + ); +}; +``` + +#### Row Action: Reschedule Item + +**Trigger**: Show "Reschedule" action when: +- `site_status === 'scheduled'` (scheduled but not yet published) +- `site_status === 'failed'` (scheduled publishing failed) + +**Modal**: Reuse `ScheduleContentModal` but: +- Title: "Reschedule Content Publishing" +- Pre-fill with existing `scheduled_publish_at` date/time +- API: `POST /v1/writer/content/{id}/reschedule/` + +**Implementation**: +```typescript +const handleRescheduleContent = async (contentId: number, scheduledDate: string) => { + const response = await fetchAPI(`/v1/writer/content/${contentId}/reschedule/`, { + method: 'POST', + body: JSON.stringify({ scheduled_at: scheduledDate }), + }); + + // Update local state + setContent(prevContent => + prevContent.map(c => c.id === contentId ? { + ...c, + site_status: 'scheduled', + scheduled_publish_at: scheduledDate, + } : c) + ); + + toast.success('Rescheduled successfully'); +}; +``` + +#### Row Action: Unschedule Item + +**Trigger**: Show when `site_status === 'scheduled'` + +**Confirmation Modal**: +``` +┌────────────────────────────────────────┐ +│ ⚠️ Unschedule Content? │ +├────────────────────────────────────────┤ +│ Are you sure you want to unschedule │ +│ "Article Title Here"? │ +│ │ +│ Current schedule: Jan 15, 2025 9:00 AM │ +│ │ +├────────────────────────────────────────┤ +│ [Cancel] [Unschedule] │ +└────────────────────────────────────────┘ +``` + +**API Call**: +```typescript +const handleUnscheduleContent = async (contentId: number) => { + await fetchAPI(`/v1/writer/content/${contentId}/unschedule/`, { + method: 'POST', + }); + + setContent(prevContent => + prevContent.map(c => c.id === contentId ? { + ...c, + site_status: 'not_published', + scheduled_publish_at: null, + } : c) + ); + + toast.success('Unscheduled successfully'); +}; +``` + +#### Updated Row Actions for Approved Page + +**File**: `frontend/src/config/pages/approved.config.tsx` + +**Dynamic row actions based on content state** (platform-agnostic): +```typescript +const getRowActions = (content: Content, siteName: string) => { + const actions = []; + + // Always available + actions.push({ label: 'View', value: 'view', icon: EyeIcon }); + actions.push({ label: 'Edit', value: 'edit', icon: PencilIcon }); + + // Publishing actions based on site_status (platform-agnostic) + if (content.site_status === 'not_published') { + actions.push({ label: 'Publish Now', value: 'publish_site', icon: RocketLaunchIcon }); + actions.push({ label: 'Schedule', value: 'schedule', icon: CalendarIcon }); + } else if (content.site_status === 'scheduled') { + actions.push({ label: 'Reschedule', value: 'reschedule', icon: CalendarIcon }); + actions.push({ label: 'Unschedule', value: 'unschedule', icon: XIcon }); + actions.push({ label: 'Publish Now', value: 'publish_site', icon: RocketLaunchIcon }); + } else if (content.site_status === 'failed') { + actions.push({ label: 'Publish Now', value: 'publish_site', icon: RocketLaunchIcon }); + actions.push({ label: 'Reschedule', value: 'reschedule', icon: CalendarIcon }); + } else if (content.site_status === 'published' && content.external_url) { + // Use actual site name for dynamic label + actions.push({ + label: `View on ${siteName}`, + value: 'view_on_site', + icon: ExternalLinkIcon + }); + } + + actions.push({ label: 'Delete', value: 'delete', icon: TrashBinIcon, danger: true }); + + return actions; +}; +``` + +#### Bulk Actions for Scheduling + +**New bulk action**: "Schedule Selected" + +**Modal**: `BulkScheduleModal` (new) +``` +┌────────────────────────────────────────┐ +│ 📅 Schedule 5 Articles │ +├────────────────────────────────────────┤ +│ Schedule all selected articles for: │ +│ │ +│ Date: [MM/DD/YYYY ▼] │ +│ Time: [HH:MM AM/PM ▼] │ +│ │ +│ Preview: January 15, 2025 at 9:00 AM │ +│ │ +│ ⚠️ All 5 articles will be scheduled │ +│ for the same date and time. │ +│ │ +├────────────────────────────────────────┤ +│ [Cancel] [Schedule All] │ +└────────────────────────────────────────┘ +``` + +**Implementation**: +```typescript +const handleBulkSchedule = async (contentIds: number[], scheduledDate: string) => { + let successCount = 0; + let failedCount = 0; + + for (const contentId of contentIds) { + try { + await fetchAPI(`/v1/writer/content/${contentId}/schedule/`, { + method: 'POST', + body: JSON.stringify({ scheduled_publish_at: scheduledDate }), + }); + successCount++; + } catch (error) { + console.error(`Failed to schedule content ${contentId}:`, error); + failedCount++; + } + } + + if (successCount > 0) { + toast.success(`Scheduled ${successCount} item(s)`); + } + if (failedCount > 0) { + toast.warning(`${failedCount} item(s) failed to schedule`); + } + + loadContent(); // Refresh +}; +``` + +### 5.2 Scheduling in Content Calendar Page + +**Current State**: Scheduling via drag-and-drop works well + +**Enhancement**: Add "Edit Schedule" button to scheduled items + +**Design**: +- Each scheduled item in calendar has an "Edit" button +- Opens `ScheduleContentModal` with existing date/time pre-filled +- User can change date/time +- Calls reschedule API + +**Implementation** (add to calendar item render): +```typescript +{item.site_status === 'scheduled' && ( + } + onClick={() => openRescheduleModal(item)} + title="Edit schedule" + size="sm" + /> +)} +``` + +**Failed Items Handling**: +- Show failed items in a separate section (not on calendar) +- Red error badge +- Two buttons: "Reschedule" and "Publish Now" +- Clicking "Reschedule" opens `ScheduleContentModal` + +**Design**: +``` +┌──────────────────────────────────────────────────────────┐ +│ ❌ Failed Scheduled Publications (2) │ +├──────────────────────────────────────────────────────────┤ +│ ⚠️ Article Title 1 │ +│ Site: My WordPress Blog │ +│ Scheduled: Jan 13, 2025 9:00 AM │ +│ Error: Publishing API error: Invalid credentials │ +│ [Reschedule] [Publish Now] │ +│ │ +│ ⚠️ Article Title 2 │ +│ Site: My Shopify Store │ +│ Scheduled: Jan 13, 2025 10:00 AM │ +│ Error: Network timeout │ +│ [Reschedule] [Publish Now] │ +└──────────────────────────────────────────────────────────┘ +``` + +### 5.3 Scheduling in ContentView Template + +**File**: `frontend/src/templates/ContentView.tsx` (if exists) + +**Note**: File not found during analysis - may not exist or may be different path + +**If this is the content detail view**: +- Add "Schedule" button in header actions (if `site_status === 'not_published'`) +- Add "Reschedule" button (if `site_status === 'scheduled'` or `site_status === 'failed'`) +- Add "Publish Now" button (if approved) +- Show current schedule prominently if scheduled + +**Alternate**: If ContentView doesn't exist, check if content detail page is at: +- `frontend/src/pages/Writer/ContentDetail.tsx` +- Or individual content pages use a different template + +--- + +## 6. Failed Content Handling + +### 6.1 Identifying Failed Content + +**Query**: Content where `site_status = 'failed'` + +**Display Locations**: +1. **Approved Page**: Filter by `site_status = 'failed'` +2. **Content Calendar**: Separate "Failed" section +3. **Dashboard Widget**: "Failed Publications" count + +### 6.2 Failed Item UI Treatment + +**Visual Indicators**: +- ❌ Red error badge: "Failed" +- 🕐 Show original scheduled time +- 📄 Show error message (truncated) +- 🔄 Retry options: "Publish Now" or "Reschedule" + +**Row Actions for Failed Items**: +```typescript +{ + label: 'Publish Now', + value: 'publish_site', + icon: RocketLaunchIcon, +} +{ + label: 'Reschedule', + value: 'reschedule', + icon: CalendarIcon, +} +{ + label: 'View Error Details', + value: 'view_error', + icon: ErrorIcon, +} +``` + +**Error Details Modal**: +``` +┌────────────────────────────────────────────────────────────┐ +│ ❌ Publishing Error Details │ +├────────────────────────────────────────────────────────────┤ +│ Content: "Article Title Here" │ +│ Site: My WordPress Blog (WordPress) │ +│ Scheduled: January 13, 2025 at 9:00 AM │ +│ Failed: January 13, 2025 at 9:05 AM │ +│ │ +│ Error Message: │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ Publishing API error: Invalid credentials │ │ +│ │ │ │ +│ │ The publishing site returned a 403 Forbidden error. │ │ +│ │ Please check the API key in Site Settings. │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ │ +│ Actions: │ +│ [Fix Site Settings] [Publish Now] [Reschedule] [Close] │ +└────────────────────────────────────────────────────────────┘ +``` + +### 6.3 Retry Logic + +**Publish Now** (from failed state): +- Opens `PublishingProgressModal` +- Same flow as normal publish +- On success: `site_status` → `published`, clear error +- On failure: Update error message, keep as `failed` + +**Reschedule** (from failed state): +- Opens `ScheduleContentModal` +- Pre-fill with original scheduled time (or next day) +- On save: `site_status` → `scheduled`, clear error +- Celery task will retry at new scheduled time + +--- + +## 7. Implementation Phases + +### Phase 1: Publishing Progress Modals (Week 1-2) + +**Tasks**: +1. Create `PublishingProgressModal.tsx` component (platform-agnostic) +2. Create `BulkPublishingModal.tsx` component (platform-agnostic) +3. Create `PublishLimitModal.tsx` component (new - limit validation) +4. Update `Approved.tsx`: + - Integrate single publish modal + - Integrate bulk publish modal (max 5 items) + - Add limit validation: show `PublishLimitModal` if > 5 selected + - Rename `handleBulkPublishWordPress` to `handleBulkPublishToSite` + - Update action names: `publish_wordpress` → `publish_site` + - Display actual site name in UI + - Add "Schedule Selected" bulk action (no limit) +5. Test with WordPress, Shopify, and custom sites +6. Handle platform-specific error states and retry logic +7. Test limit validation (6+ items selected) + +**Deliverables**: +- ✅ Single publish with progress feedback (all platforms) +- ✅ Bulk publish with queue progress (max 5 items) +- ✅ Publishing limit validation modal +- ✅ Error handling with retry +- ✅ Success state with "View on [Site Name]" link +- ✅ Platform-agnostic terminology throughout + +### Phase 2: Remove Publish from Review (Week 2) + +**Tasks**: +1. Update `Review.tsx`: + - Remove `handlePublishSingle()` function + - Remove `handlePublishBulk()` function + - Remove publish actions from row actions + - Update primary action to "Approve" +2. Update `review.config.tsx`: + - Remove publish actions from rowActions array +3. Test workflow: Review → Approve → Approved → Publish + +**Deliverables**: +- ✅ Review page only approves content +- ✅ No publishing from Review page +- ✅ Clearer workflow separation + +### Phase 3: Scheduling UI in Approved Page (Week 3) + +**Tasks**: +1. Create `ScheduleContentModal.tsx` component +2. Create `BulkScheduleModal.tsx` component (manual date/time) +3. Create `BulkSchedulePreviewModal.tsx` component (site defaults preview) +4. Create backend endpoint: `POST /v1/writer/content/bulk_schedule/` +5. Create backend endpoint: `POST /v1/writer/content/bulk_schedule_preview/` +6. Update `Approved.tsx`: + - Add "Schedule" row action + - Add "Reschedule" row action (conditional) + - Add "Unschedule" row action (conditional) + - Add "Schedule Selected" bulk action (uses site defaults) + - Dynamic row actions based on `site_status` + - Link to site settings from schedule preview modal +7. Test scheduling, rescheduling, unscheduling +8. Test bulk scheduling with site defaults (10+ items) +9. Verify site settings integration + +**Deliverables**: +- ✅ Schedule content for future publishing +- ✅ Reschedule existing scheduled content +- ✅ Unschedule content +- ✅ Bulk scheduling with site default settings +- ✅ Schedule preview before confirmation +- ✅ Link to Site Settings → Publishing tab +- ✅ No limit on scheduled items + +### Phase 4: Failed Content Handling (Week 3-4) + +**Tasks**: +1. Add failed content filter to Approved page +2. Create failed items section in Content Calendar +3. Create `ErrorDetailsModal.tsx` for viewing full errors +4. Add retry logic for failed items +5. Update row actions for failed items + +**Deliverables**: +- ✅ Visual indication of failed content +- ✅ Error details modal +- ✅ Retry/Reschedule options +- ✅ Clear error messages + +### Phase 5: Content Calendar Enhancements (Week 4) + +**Tasks**: +1. Add "Edit Schedule" button to calendar items +2. Add failed items section to calendar +3. Improve calendar item tooltips +4. Test drag-drop with new scheduling modals + +**Deliverables**: +- ✅ Edit scheduled items from calendar +- ✅ Failed items displayed prominently +- ✅ Improved UX for scheduling + +### Phase 6: Testing & Documentation (Week 5) + +**Tasks**: +1. E2E testing of all workflows +2. Test error scenarios +3. Update user documentation +4. Create user guide for new scheduling features +5. Performance testing for bulk operations + +**Deliverables**: +- ✅ All features tested and working +- ✅ Documentation updated +- ✅ User guide created +- ✅ Performance benchmarks + +--- + +## 8. Technical Considerations + +### 8.1 State Management + +**Local State** (per page): +- Modal open/close states +- Queue items for bulk operations +- Selected date/time for scheduling + +**Global State** (if needed): +- Active publishing operations (prevent duplicate publishes) +- Recent errors (for error dashboard) + +**Recommendation**: Use local component state with React hooks. No need for Redux/Zustand unless state sharing becomes complex. + +### 8.2 Performance + +**Bulk Operations**: +- Sequential processing (not parallel) to avoid API rate limits +- Show progress per item +- Allow cancellation (future enhancement) + +**Calendar View**: +- Lazy load calendar months +- Limit displayed items per date (show "X more..." if > 3) +- Virtual scrolling for large lists + +**Polling** (if needed for async publishing): +- Currently publishing is synchronous, so no polling needed +- If made async in future, poll task status like ImageQueueModal does + +### 8.3 Error Handling + +**Network Errors**: +- Catch and display user-friendly messages +- Log full error to console for debugging +- Provide retry button + +**Platform API Errors**: +- Parse platform-specific error messages (WordPress, Shopify, Custom API) +- Highlight common issues (invalid credentials, missing fields, rate limits) +- Link to Site Settings for credential fixes +- Show platform type in error messages for clarity + +**Validation Errors**: +- Validate schedule date/time (must be in future) +- Validate content is approved before publishing +- Check site configuration exists (API key, domain) +- Verify platform-specific requirements are met + +### 8.4 Accessibility + +**Modals**: +- Keyboard navigation (Tab, Esc) +- ARIA labels for screen readers +- Focus trap when modal is open +- Announce status changes + +**Progress Bars**: +- ARIA role="progressbar" +- aria-valuenow, aria-valuemin, aria-valuemax +- Announce percentage changes + +**Color Indicators**: +- Don't rely only on color (use icons too) +- High contrast for dark mode +- Color-blind friendly palette + +--- + +## 9. UI/UX Best Practices + +### 9.1 Loading States + +- Show skeleton loaders for content lists +- Disable buttons during API calls +- Show spinner in button: "Publishing..." (with spinner icon) +- Progress bars for multi-step operations + +### 9.2 Success Feedback + +- Green toast notification: "Published successfully" +- Green checkmark icon in modal +- Auto-refresh content list on success +- Optional: Confetti animation for first publish + +### 9.3 Error Feedback + +- Red toast notification: "Failed to publish: [error]" +- Red alert box in modal with full error +- "Copy error details" button for debugging +- "Contact support" link if needed + +### 9.4 Empty States + +- No scheduled content: Show calendar with "Drag approved content here" +- No failed content: "No failed publications" with green checkmark +- No approved content: "Approve content from Review page" + +### 9.5 Tooltips and Help Text + +- Calendar dates: "Drag content here to schedule" +- Schedule button: "Schedule for future publishing to site" +- Publish button: "Publish immediately to [Site Name]" +- Failed badge: "Publishing failed - click for details" +- Site name shown in all tooltips where relevant + +--- + +## 10. Future Enhancements + +### 10.1 Advanced Scheduling + +- **Recurring schedules**: Publish every Monday at 9 AM +- **Bulk schedule spread**: Schedule 10 items over next 2 weeks (evenly distributed) +- **Optimal timing**: AI suggests best time to publish based on traffic data +- **Timezone support**: Schedule in site's timezone (not user's) + +### 10.2 Publishing Queue Management + +- **Pause/Resume queue**: Pause all scheduled publications +- **Reorder queue**: Drag to reorder in list view +- **Priority flag**: Mark content as high priority (publish first) +- **Batch limits**: Limit to X publications per day + +### 10.3 Multi-Platform Publishing + +- **Social media**: Publish to Facebook, Twitter, LinkedIn +- **Multiple sites**: Publish to multiple sites simultaneously (WordPress, Shopify, Custom) +- **Cross-post**: Publish same content to blog + Medium + Dev.to +- **Mixed platforms**: Schedule content to WordPress site and Shopify store at different times + +### 10.4 Advanced Error Handling + +- **Auto-retry**: Retry failed publications X times before marking as failed +- **Error patterns**: Detect common errors and suggest fixes +- **Health checks**: Pre-flight check before scheduling (test credentials) + +### 10.5 Analytics Integration + +- **Publishing stats**: Track success/failure rates +- **Performance metrics**: Average time to publish +- **Engagement tracking**: Track views/clicks on published content +- **Calendar heatmap**: Visual representation of publishing activity + +--- + +## 11. Testing Checklist + +### 11.1 Single Publishing + +- [ ] Publish single approved content +- [ ] Progress modal shows correct stages +- [ ] Success state displays site URL (works for WordPress, Shopify, Custom) +- [ ] Modal shows actual site name, not platform type +- [ ] Error state displays error message +- [ ] Retry button works on failure +- [ ] Close button disabled during publish +- [ ] Content list refreshes on success +- [ ] Test with WordPress site +- [ ] Test with Shopify site +- [ ] Test with Custom site + +### 11.2 Bulk Publishing + +- [ ] Publish multiple approved content items (up to 5) +- [ ] Limit validation: 6+ items shows PublishLimitModal +- [ ] PublishLimitModal offers "Schedule Selected" option +- [ ] Single record publish (3-dot menu) has no limit +- [ ] Queue displays all items +- [ ] Sequential processing works +- [ ] Individual progress per item +- [ ] Mixed success/failure handling +- [ ] Retry individual failed items +- [ ] Close button enabled after completion +- [ ] Button tooltip shows limit info when > 5 selected + +### 11.3 Scheduling + +**Manual Scheduling:** +- [ ] Schedule single content (manual date/time) +- [ ] Schedule bulk content (manual date/time) +- [ ] Date/time picker validation +- [ ] Schedule in past shows error +- [ ] Scheduled items appear in calendar +- [ ] Drag-and-drop scheduling works +- [ ] Reschedule changes date/time +- [ ] Unschedule removes from calendar + +**Bulk Scheduling with Site Defaults:** +- [ ] Select 10+ items for scheduling (no limit) +- [ ] "Schedule Selected" button opens preview modal +- [ ] Preview shows schedule with stagger intervals +- [ ] Preview displays site settings (time, stagger, timezone) +- [ ] "Change Settings" link opens site settings in new tab +- [ ] Confirm schedule applies to all selected items +- [ ] Items appear in calendar with scheduled times +- [ ] Site settings at `/sites/{id}/settings?tab=publishing` has scheduling config +- [ ] Automation uses same scheduling config +- [ ] Bulk scheduled items respect site's daily limits (if configured) + +### 11.4 Failed Content + +- [ ] Failed items display with error badge +- [ ] Error details modal shows full error +- [ ] Retry from failed state works +- [ ] Reschedule from failed state works +- [ ] Failed section in calendar shows items +- [ ] Filter failed items in Approved page + +### 11.5 Review Page Changes + +- [ ] Publish actions removed +- [ ] Approve action works +- [ ] Bulk approve works +- [ ] No direct publishing possible +- [ ] Approved content moves to Approved page + +### 11.6 Edge Cases + +- [ ] No site credentials: Error message +- [ ] Missing API key: Error message +- [ ] Unsupported platform type: Error message +- [ ] Network timeout: Error message +- [ ] Invalid content (missing required fields): Error +- [ ] Publishing already published content: Handled +- [ ] Scheduling already scheduled content: Reschedule +- [ ] Deleting scheduled content: Removes from calendar +- [ ] Site change: Refresh content +- [ ] Platform-specific validation (WordPress categories, Shopify collections, etc.) + +**Publishing Limits:** +- [ ] Selecting exactly 5 items: Direct publish allowed +- [ ] Selecting 6 items: PublishLimitModal shown +- [ ] Selecting 10+ items: PublishLimitModal shown, suggests scheduling +- [ ] From limit modal: "Schedule Selected" opens bulk schedule preview +- [ ] From limit modal: "Go Back" closes modal, keeps selection +- [ ] Single publish (3-dot): Always allowed, no limit modal +- [ ] Bulk button disabled when no items selected +- [ ] Bulk button shows tooltip when > 5 items selected + +**Bulk Scheduling:** +- [ ] No site scheduling config: Uses defaults (9 AM, 15 min stagger) +- [ ] Site has custom schedule: Preview reflects custom settings +- [ ] Scheduling 50+ items: No error, all scheduled +- [ ] Site settings link opens correct tab +- [ ] Return from site settings: Preview refreshes with new settings + +--- + +## 12. Documentation Updates Required + +### 12.1 User Documentation + +**File**: `docs/40-WORKFLOWS/CONTENT-PUBLISHING.md` (new or update existing) + +**Sections**: +1. Publishing Workflow Overview +2. Publishing from Approved Page +3. Scheduling Content +4. Managing Scheduled Content +5. Handling Failed Publications +6. Bulk Operations +7. Troubleshooting + +### 12.2 Developer Documentation + +**File**: `docs/30-FRONTEND/PUBLISHING-MODALS.md` (new) + +**Sections**: +1. Modal Architecture +2. Component API Reference +3. State Management +4. API Integration +5. Progress Animation Logic +6. Error Handling Patterns +7. Testing Guidelines + +### 12.3 API Documentation + +**File**: `docs/20-API/PUBLISHER.md` (update existing) + +**Add**: +- Schedule endpoint: `POST /v1/writer/content/{id}/schedule/` +- Reschedule endpoint: `POST /v1/writer/content/{id}/reschedule/` +- Unschedule endpoint: `POST /v1/writer/content/{id}/unschedule/` +- Examples with request/response + +--- + +## 13. Conclusion + +This comprehensive plan provides: + +1. ✅ **Publishing Progress Modals** - Real-time feedback for single and bulk operations +2. ✅ **Publishing Limits & Validation** - Max 5 direct publish, unlimited scheduling +3. ✅ **Workflow Optimization** - Clear separation of Review (approve only) and Approved (publish) +4. ✅ **Scheduling UI** - Full scheduling/rescheduling capabilities across multiple pages +5. ✅ **Site Settings Integration** - Bulk scheduling uses default configuration +6. ✅ **Failed Content Handling** - Clear error display with retry/reschedule options +7. ✅ **Pattern Consistency** - Follows existing modal patterns from ImageQueueModal +8. ✅ **User Experience** - Intuitive, informative, and forgiving interface +9. ✅ **Platform Agnostic** - Supports WordPress, Shopify, Custom sites with unified terminology +10. ✅ **Scalable Architecture** - Easy to add new publishing platforms in the future + +### Key Success Metrics + +- **User Satisfaction**: Fewer support tickets about "where did my content go?" +- **Publishing Success Rate**: Increased from tracking and retry capabilities +- **Workflow Clarity**: Users understand: Review → Approve → Publish +- **Error Recovery**: Failed publications can be easily rescheduled +- **Time Savings**: Bulk operations with progress tracking reduce manual work + +### Next Steps + +1. Review this plan with stakeholders +2. Prioritize phases based on business needs +3. Begin Phase 1 implementation (Publishing Progress Modals) +4. Conduct user testing after Phase 3 +5. Iterate based on feedback + +--- + +**Document Version**: 1.0 +**Last Updated**: January 2025 +**Status**: Ready for Implementation