temproary docs uplaoded

This commit is contained in:
IGNY8 VPS (Salman)
2026-03-23 09:02:49 +00:00
parent cb6eca4483
commit 128b186865
113 changed files with 68897 additions and 0 deletions

View File

@@ -0,0 +1,403 @@
# Automation & Publishing Scheduling System
> **Last Updated:** January 18, 2026
> **System:** IGNY8 Celery Task Scheduling
---
## Overview
IGNY8 uses **Celery Beat** to schedule two distinct but related systems:
1. **Automation Pipeline** - Processes content through 7 stages (Keywords → Published)
2. **Publishing Scheduler** - Schedules and publishes approved content to WordPress
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ CELERY BEAT SCHEDULER │
│ (Persistent Schedule Store) │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────┼─────────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Every Hour │ │ Every Hour │ │ Every 5 min │
│ at :05 │ │ at :00 │ │ │
└───────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ check_ │ │schedule_approved│ │process_scheduled│
│ scheduled_ │ │ _content │ │ _publications │
│ automations │ │ │ │ │
└───────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Automation │ │ Content gets │ │ Publishes to │
│ Pipeline │ │ scheduled_ │ │ WordPress via │
│ (7 stages) │ │ publish_at set │ │ API │
└───────────────┘ └─────────────────┘ └─────────────────┘
```
---
## 1. Automation Scheduling
### Celery Task Configuration
| Task Name | Schedule | Purpose |
|-----------|----------|---------|
| `automation.check_scheduled_automations` | Every hour at `:05` | Check if any automation configs should run |
| `automation.check_test_triggers` | Every 5 minutes | Check for admin test triggers (exits early if none) |
### How It Works
```
┌──────────────────────────────────────────────────────────────────────────┐
│ AUTOMATION SCHEDULING FLOW │
└──────────────────────────────────────────────────────────────────────────┘
Celery Beat AutomationConfig Result
│ │ │
│ (Every hour at :05) │ │
▼ │ │
┌─────────────┐ │ │
│ 02:05 check │ ─── Hour ────► scheduled_hour == 2? │
│ 03:05 check │ ─── Hour ────► scheduled_hour == 3? │
│ 04:05 check │ ─── Hour ────► scheduled_hour == 4? │
│ ... │ │ │
└─────────────┘ │ │
▼ │
┌────────────────┐ │
│ Match Found? │ │
└────────────────┘ │
│ │ │
Yes No │
│ │ │
▼ └─► Skip │
┌────────────────┐ │
│ Check Blocks: │ │
│ • 23hr block │ │
│ • Already │ │
│ running? │ │
└────────────────┘ │
│ │
Pass │
│ │
▼ │
┌────────────────┐ │
│ Start Run │ ────────────────────────► Run Started
│ • Set last_run │
│ • Queue task │
└────────────────┘
```
### Hourly Matching Logic
The scheduler runs at `:05` of every hour and checks if `scheduled_hour == current_hour`:
| Celery Runs At | Checks Hour | Example Config |
|----------------|-------------|----------------|
| `02:05` | `2` | `scheduled_time = 02:00` → Matches |
| `03:05` | `3` | `scheduled_time = 03:00` → Matches |
| `14:05` | `14` | `scheduled_time = 14:00` (2 PM) → Matches |
**Note:** Users select hour only (1-12 with AM/PM in UI), stored as `HH:00` format. The `:05` offset ensures the check happens after the configured hour begins.
### Test Mode (Admin Feature)
Admins can trigger automations without waiting for the hourly schedule:
```
┌──────────────────────────────────────────────────────────────────────────┐
│ TEST TRIGGER FLOW │
└──────────────────────────────────────────────────────────────────────────┘
Admin Action Celery (Every Minute) Result
│ │ │
▼ │ │
┌─────────────────┐ │ │
│ Enable test mode│ │ │
│ Set trigger time│ │ │
│ (or "now") │ │ │
└─────────────────┘ │ │
│ │ │
└───────────────────────────────────▼ │
┌─────────────────┐ │
│check_test_ │ │
│triggers task │ │
└─────────────────┘ │
│ │
▼ │
┌─────────────────┐ │
│test_trigger_at │ │
│<= now? │ │
└─────────────────┘ │
│ │ │
Yes No │
│ └─► Wait │
▼ │
┌─────────────────┐ │
│Start test run │──────────────► Run Started
│trigger_type= │ (type='test')
│'test' │
│Clear trigger_at │
└─────────────────┘
```
**Test mode fields:**
- `test_mode_enabled` - Enable test functionality for this config
- `test_trigger_at` - When to trigger (set to `now` for immediate)
**Admin actions:**
- 🧪 **Trigger test run** - Sets both fields and triggers at next minute
- 🧹 **Clear test mode** - Disables test mode and clears trigger
### Frequency Rules
| Frequency | When It Runs | Additional Condition |
|-----------|--------------|----------------------|
| `daily` | Every day | `scheduled_hour == current_hour` |
| `weekly` | Mondays only | `weekday() == 0` + hour match |
| `monthly` | 1st of month only | `day == 1` + hour match |
### Duplicate Prevention (23-Hour Block)
After a successful run, the config won't run again for **23 hours**:
```python
if config.last_run_at:
time_since_last_run = now - config.last_run_at
if time_since_last_run < timedelta(hours=23):
# SKIP - already ran recently
```
### Schedule Change Behavior
When `scheduled_time`, `frequency`, or `is_enabled` changes:
- `last_run_at` is reset to `None`
- `next_run_at` is recalculated
- Automation becomes eligible immediately at the next hourly check
**Example:**
```
Current time: 12:30
You change scheduled_time from 02:00 → 12:00 (12 PM)
Hour 12 was already checked at 12:05
→ Automation will run tomorrow at 12:05 (daily) or next valid day (weekly/monthly)
```
**Pro tip:** Use test mode to trigger immediately after schedule changes.
---
## 2. Publishing Scheduling
### Celery Task Configuration
| Task Name | Schedule | Purpose |
|-----------|----------|---------|
| `publishing.schedule_approved_content` | Every hour at `:00` | Schedule approved content for future publishing |
| `publishing.process_scheduled_publications` | Every 5 min | Publish content where `scheduled_publish_at <= now` |
### Publishing Flow
```
┌──────────────────────────────────────────────────────────────────────────┐
│ PUBLISHING SCHEDULING FLOW │
└──────────────────────────────────────────────────────────────────────────┘
EVERY HOUR (:00) EVERY 5 MINUTES
│ │
▼ ▼
┌────────────────────────┐ ┌────────────────────────┐
│schedule_approved_content│ │process_scheduled_ │
│ │ │publications │
└────────────────────────┘ └────────────────────────┘
│ │
▼ │
┌────────────────────────┐ │
│ For each Site with │ │
│ auto_publish_enabled: │ │
│ │ │
│ 1. Find approved │ │
│ content │ │
│ 2. Calculate slots │ │
│ based on settings │ │
│ 3. Set scheduled_ │ │
│ publish_at │ │
│ 4. Set site_status= │ │
│ 'scheduled' │ │
└────────────────────────┘ │
│ │
▼ ▼
┌───────────────────────────────────────────────┐
│ DATABASE │
│ ┌─────────────────────────────────────────┐ │
│ │ Content Table │ │
│ │ • status = 'approved' │ │
│ │ • site_status = 'scheduled' │ │
│ │ • scheduled_publish_at = <datetime> │ │
│ └─────────────────────────────────────────┘ │
└───────────────────────────────────────────────┘
┌────────────────────────┐
│ process_scheduled_ │
│ publications │
│ │
│ WHERE: │
│ • site_status = │
│ 'scheduled' │
│ • scheduled_publish_at │
│ <= NOW │
└────────────────────────┘
┌────────────────────────┐
│ Publish to WordPress │
│ via PublisherService │
│ │
│ On Success: │
│ • site_status = │
│ 'published' │
│ • Set wp_post_id │
└────────────────────────┘
```
### Scheduling Modes
| Mode | Behavior |
|------|----------|
| `immediate` | Content scheduled for `now`, picked up within 5 minutes |
| `time_slots` | Content scheduled at specific times (e.g., 9am, 2pm, 6pm) |
| `stagger` | Content spread evenly across publish hours |
### Publishing Limits
| Limit | Description |
|-------|-------------|
| `daily_publish_limit` | Max posts per day |
| `weekly_publish_limit` | Max posts per week |
| `monthly_publish_limit` | Max posts per month |
| `queue_limit` | Max items to schedule at once (default: 100) |
---
## 3. System Relationship
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ COMPLETE CONTENT LIFECYCLE │
└─────────────────────────────────────────────────────────────────────────────┘
AUTOMATION PIPELINE PUBLISHING PIPELINE
(Every 15 min check) (Hourly + Every 5 min)
│ │
▼ │
┌─────────────────┐ │
│ Stage 1-6 │ │
│ Keywords → │ │
│ Content Created │ │
└─────────────────┘ │
│ │
▼ │
┌─────────────────┐ │
│ Stage 7 │ │
│ Auto-Approval │ status='approved' │
│ (if enabled) │ ─────────────────────────────────► │
└─────────────────┘ │
┌─────────────────────┐
│schedule_approved_ │
│content (hourly) │
│ │
│ Sets: │
│ • scheduled_publish │
│ _at │
│ • site_status = │
│ 'scheduled' │
└─────────────────────┘
┌─────────────────────┐
│process_scheduled_ │
│publications (5 min) │
│ │
│ Publishes to WP │
│ • site_status = │
│ 'published' │
└─────────────────────┘
```
---
## 4. Celery Beat Schedule Summary
| Task | Schedule | Crontab |
|------|----------|---------|
| `automation.check_scheduled_automations` | Every 15 min | `minute='0,15,30,45'` |
| `publishing.schedule_approved_content` | Hourly | `minute=0` |
| `publishing.process_scheduled_publications` | Every 5 min | `minute='*/5'` |
---
## 5. Key Configuration Tables
### AutomationConfig
| Field | Type | Purpose |
|-------|------|---------|
| `is_enabled` | Boolean | Enable/disable scheduling |
| `frequency` | Choice | `daily`, `weekly`, `monthly` |
| `scheduled_time` | Time | When to run (e.g., `02:00`) |
| `last_run_at` | DateTime | Last successful run (23hr block) |
| `next_run_at` | DateTime | Calculated next run time |
### PublishingSettings
| Field | Type | Purpose |
|-------|------|---------|
| `auto_publish_enabled` | Boolean | Enable auto-scheduling |
| `scheduling_mode` | Choice | `immediate`, `time_slots`, `stagger` |
| `publish_days` | Array | `['mon', 'tue', ...]` |
| `publish_time_slots` | Array | `['09:00', '14:00', '18:00']` |
| `daily_publish_limit` | Integer | Max posts/day |
---
## 6. Troubleshooting
### Automation Didn't Run
| Check | Solution |
|-------|----------|
| `is_enabled = False` | Enable the automation |
| Time not in current window | Wait for next window or change time |
| `last_run_at` within 23hrs | Wait for 23hr block to expire |
| Another run in progress | Wait for current run to complete |
| Config updated after window | Schedule moved to next occurrence |
### Content Not Publishing
| Check | Solution |
|-------|----------|
| `auto_publish_enabled = False` | Enable in PublishingSettings |
| Content `status != 'approved'` | Approve content first |
| No `wp_api_key` on Site | Configure WordPress integration |
| Daily/weekly limit reached | Wait for limit reset or increase |
---
## 7. Logs
Monitor these logs for scheduling issues:
```bash
# Automation scheduling
docker logs igny8_celery_worker 2>&1 | grep "AutomationTask"
# Publishing scheduling
docker logs igny8_celery_worker 2>&1 | grep "schedule_approved_content\|process_scheduled"
```

View File

@@ -0,0 +1,338 @@
# Content Pipeline Workflow
**Last Verified:** January 20, 2026
**Version:** 1.8.4
---
## Overview
The IGNY8 content pipeline transforms raw keywords into published WordPress articles through a multi-stage workflow. This can run manually (step-by-step) or automatically via the Automation module.
---
## Pipeline Stages
```
┌─────────────────────────────────────────────────────────────────────────┐
│ CONTENT PIPELINE │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ KEYWORDS │───►│ CLUSTERS │───►│ IDEAS │───►│ TASKS │ │
│ │ Stage 1 │ │ Stage 2 │ │ Stage 3 │ │ Stage 4 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ CONTENT │───►│ IMAGES │───►│ REVIEW │───►│ PUBLISH │ │
│ │ Stage 5 │ │ Stage 6 │ │ Stage 7 │ │ Stage 8 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## Stage 1: Keywords
**Module:** Planner
**Status Values:** `new`, `mapped`
### Input
- Seed keywords (manually added or from SEO tools)
- Optional: search volume, difficulty, CPC data
### Process
1. User adds keywords via UI or bulk import
2. Keywords validated and deduplicated
3. Assigned to site + sector
### Output
- Keyword records in `Keyword` model
- Status: `pending`
### Credit Usage
- None (free operation)
---
## Stage 2: Clustering
**Module:** Planner
**AI Function:** `AutoClusterKeywords`
### Input
- Selected pending keywords (2-100)
### Process
1. AI analyzes semantic relationships
2. Groups keywords by topic/intent
3. Creates cluster with name + description
### Output
- `Cluster` records created
- Keywords linked to clusters
- Keyword status → `clustered`
### Credit Usage
- Credits deducted based on AI tokens used (see AIModelConfig)
---
## Stage 3: Ideas
**Module:** Planner
**AI Function:** `GenerateContentIdeas`
### Input
- Cluster with 2+ keywords
### Process
1. AI generates content idea titles
2. Creates brief description for each
3. Suggests primary + secondary keywords
### Output
- `ContentIdea` records created
- Status: `pending`
### Credit Usage
- Credits deducted based on AI tokens used (see AIModelConfig)
---
## Stage 4: Tasks
**Module:** Writer
**Status Values:** `pending`, `in_progress`, `completed`, `cancelled`
### Input
- Selected content ideas
### Process
1. Ideas converted to tasks
2. Task gets content brief + keywords
3. Optional: set due date, assign user
### Output
- `Task` records created
- Status: `pending`
- Ideas status → `used`
### Credit Usage
- None (free operation)
---
## Stage 5: Content Generation
**Module:** Writer
**AI Function:** `GenerateContent`
### Input
- Task with title + keywords + brief
### Process
1. AI generates full article content
2. Creates structured HTML output
3. Adds meta title + description
### Output
- `Content` record created
- Full HTML body
- SEO metadata
- Task status → `completed`
### Credit Usage
- Credits deducted based on AI tokens used (see AIModelConfig)
---
## Stage 6: Image Generation
**Module:** Writer
**AI Function:** `GenerateImages`
### Input
- Content record
- Number of images (default: 1-3)
### Process
1. AI analyzes content for image prompts
2. Generates images via DALL-E/Runware
3. Creates thumbnail + full versions
### Output
- `ContentImage` records created
- Image URLs + alt text
- Featured image assigned
### Credit Usage
- Credits deducted per image (see AIModelConfig.credits_per_image)
---
## Stage 7: Review
**Module:** Writer
**Status Values:** `draft`, `review`, `approved`, `published`
### Input
- Generated content + images
### Process
1. Content displayed in rich editor
2. User reviews + edits if needed
3. User approves for publishing
### Output
- Content status → `approved`
- Any manual edits saved
### Credit Usage
- None (free operation)
- Regeneration deducts credits based on AI model used
---
## Stage 8: Publishing
**Module:** Publisher
**Integration:** WordPress REST API
### Input
- Approved content
- WordPress integration credentials
### Process
1. Content formatted for WordPress
2. Images uploaded to WP media
3. Post created with categories/tags
4. Status set to draft/published
### Output
- `PublishingRecord` created
- WordPress post ID stored
- Content status → `published`
### Credit Usage
- None (free operation)
---
## Automation Mode
When running via Automation module:
1. **Configuration** - Set limits per stage
2. **Execution** - Pipeline runs automatically
3. **Pacing** - Configurable delays between operations
4. **Monitoring** - Real-time status updates
### Automation Config Options
```
Stage Limits:
- clustering_limit: Max keywords to cluster
- ideas_limit: Max ideas to generate
- content_limit: Max content to generate
- image_limit: Max images to generate
- publish_limit: Max content to publish
Timing:
- delay_between_operations: Seconds between API calls
- max_runtime: Maximum run duration
Behavior:
- auto_approve: Skip review stage
- auto_publish: Publish immediately
- stop_on_error: Halt pipeline on failure
```
---
## Data Flow Diagram
```
Seed Keywords
┌─────────────────┐
│ Keyword │ status: pending
│ Model │ belongs_to: site, sector
└────────┬────────┘
│ AI: AutoCluster
┌─────────────────┐
│ Cluster │ keywords: [...]
│ Model │ belongs_to: site, sector
└────────┬────────┘
│ AI: GenerateIdeas
┌─────────────────┐
│ ContentIdea │ primary_keyword, secondaries
│ Model │ cluster_id, status
└────────┬────────┘
│ Convert to Task
┌─────────────────┐
│ Task │ idea_id, brief
│ Model │ assigned_to, status
└────────┬────────┘
│ AI: GenerateContent
┌─────────────────┐
│ Content │ task_id, body, meta
│ Model │ status: draft
└────────┬────────┘
│ AI: GenerateImages
┌─────────────────┐
│ ContentImage │ content_id, url
│ Model │ alt_text, is_featured
└────────┬────────┘
│ User Review
┌─────────────────┐
│ Content │ status: approved
│ (updated) │
└────────┬────────┘
│ WordPress API
┌─────────────────┐
│PublishingRecord │ content_id, wp_post_id
│ Model │ status, published_at
└─────────────────┘
```
---
## Error Handling
| Stage | Common Errors | Recovery |
|-------|---------------|----------|
| Clustering | API timeout | Retry with smaller batch |
| Ideas | API rate limit | Wait and retry |
| Content | Insufficient credits | Add credits, retry |
| Images | Image API failure | Skip images, continue |
| Publish | WordPress auth fail | Reauth integration |
---
## Monitoring
### Pipeline Stats (Dashboard)
- Keywords pending clustering
- Ideas pending task creation
- Tasks pending generation
- Content pending review
- Content pending publish
### Automation Logs
- Run ID + timestamps
- Stage + item processed
- Success/failure status
- Credit deductions
- Error messages

View File

@@ -0,0 +1,522 @@
# Content Publishing Workflow Guide
**Last Updated**: January 20, 2026
**Version**: 1.8.4
**Status**: Production
**Audience**: Content Editors, Publishers, Site Administrators
---
## Overview
This guide covers the complete content publishing workflow, from content creation through multi-platform publishing (WordPress, Shopify, Custom Sites). The system supports both immediate publishing and scheduled publishing with progress tracking and error recovery.
---
## Publishing Workflow States
### Content Status Flow
```
Draft → Review → Approved → Published
```
- **Draft**: Content is being written/edited
- **Review**: Content submitted for review by editors
- **Approved**: Content approved and ready for publishing
- **Published**: Content successfully published to site
### Site Publishing Status
- **not_published**: Content has never been published
- **scheduled**: Content scheduled for future publishing
- **publishing**: Currently being published (in-progress)
- **published**: Successfully published to site
- **failed**: Publishing attempt failed (needs retry/reschedule)
---
## 1. Publishing from Approved Page
### Single Content Publishing
**Steps:**
1. Navigate to **Writer → Approved**
2. Find the content you want to publish
3. Click the **3-dot menu** on the content row
4. Select **"Publish Now"**
5. Publishing progress modal appears with real-time status:
- 📄 Preparing content (0-25%)
- 🚀 Uploading to site (25-50%)
- ⚙️ Processing response (50-75%)
- ✓ Finalizing (75-100%)
6. On success:
- Green checkmark displayed
- "View on [Site Name]" button available
- Content marked as published
7. On failure:
- Error message displayed
- **Retry** button available
- **Close** button to exit
**Features:**
- Real-time progress tracking
- Platform-agnostic (works with WordPress, Shopify, Custom sites)
- Automatic error recovery
- Direct link to published content
- No limit on single item publishing
### Bulk Publishing (Max 5 Items)
**Steps:**
1. Navigate to **Writer → Approved**
2. Select 2-5 content items using checkboxes
3. Click **"Publish to Site"** button at top
4. Bulk publishing modal shows queue with individual progress bars
5. Items process sequentially (one at a time)
6. Each item shows:
- Progress bar (0-100%)
- Current status (Preparing/Uploading/Processing/Finalizing)
- Success: Green checkmark + published URL
- Failure: Red X + error message + Retry button
7. Summary at bottom: "X completed, Y failed, Z pending"
8. Cannot close modal until all items complete
**Publishing Limit:**
- **Direct bulk publish**: Maximum 5 items at once
- **Reason**: Prevents server overload and API rate limiting
- **For more items**: Use "Schedule Selected" instead (no limit)
**If you select 6+ items:**
```
⚠️ Publishing Limit Exceeded Modal appears:
- Shows you selected X items (over 5 limit)
- Options:
1. Deselect items to publish ≤5
2. Use "Schedule Selected" instead (no limit)
- Tip: Scheduling is better for large batches
```
---
## 2. Scheduling Content
### Manual Scheduling (Single Item)
**Steps:**
1. Navigate to **Writer → Approved**
2. Click **3-dot menu** on content row
3. Select **"Schedule"**
4. Schedule Content Modal appears:
- **Schedule Date**: Pick date from calendar
- **Schedule Time**: Set time (HH:MM AM/PM)
- **Preview**: Shows "January 15, 2025 at 9:00 AM"
5. Click **"Schedule"** to confirm
6. Content appears in **Publisher → Content Calendar**
7. Celery task will auto-publish at scheduled time (runs every 5 minutes)
**Default Time**: 9:00 AM on selected date
### Bulk Scheduling (Unlimited Items)
**Steps:**
1. Navigate to **Writer → Approved**
2. Select any number of items (no limit!)
3. Click **"Schedule Selected"** button
4. Bulk Schedule Preview Modal shows:
- Your site's default schedule settings
- Start time (e.g., 9:00 AM)
- Stagger interval (e.g., 15 minutes between each)
- First and last publish times
- List of all items with scheduled times
5. Options:
- **"Change Settings"**: Opens Site Settings → Publishing tab in new tab
- **"Confirm Schedule"**: Applies schedule to all items
6. All items scheduled and appear in calendar
**Site Settings Integration:**
- Go to **Sites → [Your Site] → Settings → Publishing** tab
- Configure:
- **Auto-publish Schedule**: Time of day (e.g., 9:00 AM)
- **Stagger Interval**: Minutes between each (e.g., 15 min)
- **Timezone**: Your site's timezone
- **Max Daily Publishes**: Optional limit per day
- These defaults apply to bulk scheduling automatically
**Example Schedule** (10 items, 9 AM start, 15 min stagger):
```
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
...
10. Tenth Article → Jan 17, 11:15 AM
```
### Scheduling from Content Calendar
**Drag-and-Drop Method:**
1. Navigate to **Publisher → Content Calendar**
2. Approved content appears in left sidebar
3. Drag content item to desired date on calendar
4. Item scheduled for 9:00 AM on that date automatically
5. Edit time if needed (see "Editing Schedules" below)
---
## 3. Managing Scheduled Content
### Viewing Scheduled Content
**Content Calendar View:**
- Navigate to **Publisher → Content Calendar**
- Calendar shows all scheduled items by date
- Each item displays:
- Title (truncated)
- Site name
- Scheduled time
- Status badge (Scheduled/Publishing/Published/Failed)
**List View Filter:**
- Navigate to **Writer → Approved**
- Filter by `site_status = 'scheduled'`
- Shows all scheduled items in table format
### Editing Schedules (Rescheduling)
**From Calendar View:**
1. Find scheduled item on calendar
2. Click **Pencil icon** (Edit Schedule) on item
3. Schedule Content Modal opens with current date/time pre-filled
4. Change date and/or time
5. Click **"Reschedule"**
6. Item moves to new date/time on calendar
**From Approved Page:**
1. Navigate to **Writer → Approved**
2. Click **3-dot menu** on scheduled content
3. Select **"Reschedule"**
4. Change date/time in modal
5. Click **"Reschedule"**
**From 3-Dot Menu:**
- **Reschedule**: Change to new date/time
- **Unschedule**: Cancel schedule, keep as approved
- **Publish Now**: Skip schedule, publish immediately
### Unscheduling Content
**Steps:**
1. Click **3-dot menu** on scheduled content
2. Select **"Unschedule"**
3. Confirmation modal appears:
```
⚠️ Unschedule Content?
Current schedule: Jan 15, 2025 9:00 AM
[Cancel] [Unschedule]
```
4. Click **"Unschedule"** to confirm
5. Content returns to `not_published` status
6. Stays in Approved page, can be rescheduled or published directly
---
## 4. Handling Failed Publications
### Identifying Failed Content
**Visual Indicators:**
- ❌ Red "Failed" badge
- 🕐 Shows original scheduled time
- 📄 Error message (truncated)
- 🔄 Retry options available
**Where to Find Failed Items:**
1. **Content Calendar**: Separate "Failed Scheduled Publications" section at top
2. **Approved Page**: Filter by `site_status = 'failed'`
3. **Dashboard**: "Failed Publications" widget (if configured)
### Failed Item Display (Calendar)
```
┌──────────────────────────────────────────────────────┐
│ ❌ 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] │
└──────────────────────────────────────────────────────┘
```
### Viewing Error Details
**Steps:**
1. Click **3-dot menu** on failed content
2. Select **"View Error Details"**
3. Error Details Modal shows:
- Content title and site name
- Original scheduled time
- Failure timestamp
- Full error message
- Actions: Fix Site Settings / Publish Now / Reschedule
### Recovering from Failed Publishing
**Option 1: Publish Now**
- Click **"Publish Now"** button
- Opens Publishing Progress Modal
- Attempts immediate republish
- On success: Content marked as published, error cleared
- On failure: Error message updated, stays as failed
**Option 2: Reschedule**
- Click **"Reschedule"** button
- Opens Schedule Content Modal
- Pick new date/time
- Content re-queued with status = 'scheduled'
- Celery will retry at new scheduled time
**Option 3: Fix Site Settings**
- Click **"Fix Site Settings"** button
- Opens Site Settings → Publishing tab
- Check API credentials, domain, platform settings
- Return to content and retry
### Common Error Types
| Error | Cause | Solution |
|-------|-------|----------|
| Invalid credentials | API key wrong/expired | Update API key in Site Settings |
| 403 Forbidden | Permissions issue | Check user role in site admin |
| Network timeout | Site unreachable | Check domain, retry later |
| Missing required field | Content incomplete | Edit content, fill required fields |
| Rate limit exceeded | Too many requests | Reschedule with longer intervals |
| Platform-specific error | WordPress/Shopify API issue | Check error details, consult platform docs |
---
## 5. Publishing to Multiple Platform Types
### Supported Platforms
1. **WordPress Sites**
- Uses WordPress REST API
- Requires Application Password
- Supports posts, pages, custom post types
- Categories and tags automatically synced
2. **Shopify Sites**
- Uses Shopify Admin API
- Requires API key and password
- Supports products, blog posts, pages
- Collections automatically assigned
3. **Custom Sites**
- Uses custom REST API endpoint
- Flexible authentication
- Configurable field mapping
- Platform-agnostic error handling
### Platform Configuration
**Go to**: Sites → [Your Site] → Settings
**Required Fields:**
- **Platform Type**: Select WordPress / Shopify / Custom
- **Domain**: Your site URL (e.g., https://mysite.com)
- **API Key**: Platform-specific authentication key
- **Additional Settings**: Varies by platform
**WordPress Example:**
```
Platform Type: WordPress
Domain: https://myblog.com
API Key: xxxx xxxx xxxx xxxx xxxx xxxx
Username: admin
```
**Shopify Example:**
```
Platform Type: Shopify
Domain: https://mystore.myshopify.com
API Key: shpat_xxxxxxxxxxxx
Password: shppa_xxxxxxxxxxxx
```
### Publishing Behavior by Platform
All platforms use the same unified publishing interface:
- Same progress modal
- Same error handling
- Same scheduling system
- Platform differences handled automatically in backend
---
## 6. Review Page Workflow (Approval Only)
### Important: No Publishing from Review
**Review Page Purpose**: Content approval workflow only
**Available Actions:**
- ✅ Approve (single or bulk)
- ✅ View content
- ✅ Edit content
- ✅ Delete content
- ❌ ~~Publish to Site~~ (removed)
**Why?**
- Ensures content goes through proper approval workflow
- Prevents accidental publishing of unreviewed content
- Clear separation: Review → Approve → Publish
**Workflow:**
```
1. Writer creates content (Draft)
2. Writer submits for review (Review)
3. Editor reviews and approves (Approved)
4. Publisher publishes from Approved page (Published)
```
---
## 7. Best Practices
### When to Publish Immediately
- ✅ Time-sensitive content (news, announcements)
- ✅ Small batches (1-5 items)
- ✅ Testing new content types
- ✅ Urgent fixes or updates
### When to Use Scheduling
- ✅ Large batches (6+ items)
- ✅ Content with planned publish dates
- ✅ Avoiding rate limits
- ✅ Publishing during optimal times
- ✅ Automated publishing workflows
- ✅ Content calendar planning
### Scheduling Tips
1. **Use Site Defaults**: Let system handle timing automatically
2. **Stagger Publications**: 15-30 minute intervals reduce load
3. **Check Timezone**: Ensure site timezone matches your expectations
4. **Plan Ahead**: Schedule content days/weeks in advance
5. **Monitor Failures**: Check failed items daily, fix issues promptly
6. **Test First**: Publish 1-2 items manually before bulk scheduling
### Error Prevention
1. **Verify Credentials**: Test site connection before bulk operations
2. **Check Content Quality**: Ensure all required fields filled
3. **Validate Images**: Confirm images uploaded and accessible
4. **Review Platform Requirements**: WordPress categories, Shopify collections, etc.
5. **Monitor Rate Limits**: Don't schedule too many items at once
6. **Backup Content**: Export content before large publishing operations
---
## 8. Troubleshooting
### Problem: Publishing is slow
**Solution:**
- Check network connection
- Verify site is responsive
- Use scheduling instead of bulk publish
- Check site server performance
### Problem: All items failing with same error
**Solution:**
- Check Site Settings → API credentials
- Verify domain is correct and accessible
- Test site connection manually
- Check platform API status (WordPress.org, Shopify status page)
### Problem: Items stuck in "publishing" status
**Solution:**
- Backend may be processing
- Wait 5-10 minutes (Celery runs every 5 min)
- Check backend logs for errors
- Contact system administrator if persists
### Problem: Schedule times are wrong timezone
**Solution:**
- Go to Site Settings → Publishing
- Set correct timezone for your site
- Reschedule affected items
- Future schedules will use correct timezone
### Problem: Site selector not updating calendar content
**Solution:**
- Refresh your browser (Ctrl+F5 or Cmd+Shift+R)
- This issue was fixed in latest update (Jan 2026)
- Calendar now automatically reloads when you change sites
- Check browser console for site change logs
- If problem persists, clear browser cache
**What was fixed:**
- Frontend: Fixed useEffect dependency in ContentCalendar.tsx
- Backend: Added site_id and site_status to ContentFilter
- All API queries now properly filter by selected site
### Problem: Cannot see published content on site
**Solution:**
- Click "View on [Site Name]" link to verify URL
- Check site visibility settings (not private/draft)
- Clear site cache if using caching plugin
- Verify content published to correct site
- Check platform-specific visibility settings
### Problem: Bulk publish limit blocking my workflow
**Solution:**
- Use "Schedule Selected" instead (no limit)
- Set up site default schedule for automation
- Or select ≤5 items at a time for immediate publishing
---
## 9. Keyboard Shortcuts
| Shortcut | Action |
|----------|--------|
| `Space` | Select/deselect item in table |
| `Shift + Click` | Select range of items |
| `Ctrl/Cmd + Click` | Select multiple non-contiguous items |
| `Esc` | Close modal (when not publishing) |
| `Enter` | Confirm action in modal |
---
## 10. Support and Resources
### Getting Help
- **Documentation**: `docs/40-WORKFLOWS/` (this guide)
- **API Reference**: `docs/20-API/PUBLISHER.md`
- **Developer Docs**: `docs/30-FRONTEND/PUBLISHING-MODALS.md`
- **System Status**: Check backend logs at `/admin/system/logs/`
### Contact
- **Technical Issues**: Contact system administrator
- **Content Questions**: Contact editorial team
- **Platform Setup**: Contact site owner/administrator
---
**Document Version**: 1.0
**Last Updated**: January 2026
**Status**: Production Ready

View File

@@ -0,0 +1,521 @@
# Credit System
**Last Verified:** January 20, 2026
**Version:** 1.8.4
**Status:** ✅ Complete (v1.8.3 - Two-Pool Credit System)
---
## Overview
IGNY8 uses a **two-pool credit system** (v1.8.3):
- **Plan Credits** (`account.credits`): From subscription, reset on renewal
- **Bonus Credits** (`account.bonus_credits`): Purchased, **NEVER expire**
**Usage Priority:** Plan credits consumed first, bonus credits only when plan = 0.
---
## Credit Flow (Verified Architecture)
```
┌─────────────────────────────────────────────────────────────────┐
│ CREDIT FLOW │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Plan.included_credits = Monthly allocation (e.g., 10,000) │
│ ↓ (Added on subscription renewal/approval) │
│ Account.credits = Current balance (real-time, decremented) │
│ ↓ (Decremented on each AI operation) │
│ CreditTransaction = Log of all credit changes │
│ CreditUsageLog = Detailed operation tracking │
│ │
│ THESE ARE NOT PARALLEL - They serve different purposes: │
│ • Plan.included_credits = "How many credits per month" │
│ • Account.credits = "How many credits you have RIGHT NOW" │
│ │
└─────────────────────────────────────────────────────────────────┘
```
**Where credits are added to Account.credits:**
1. `billing/views.py` - When manual payment is approved
2. `payment_service.py` - When credit package purchased
3. `credit_service.py` - Generic `add_credits()` method
---
## Simplified Limits (v1.5.0)
### Hard Limits (Never Reset)
| Limit | Plan Field | Account Field | Description |
|-------|------------|---------------|-------------|
| Sites | `max_sites` | (count of Site objects) | Maximum sites per account |
| Users | `max_users` | (count of User objects) | Maximum team members |
| Keywords | `max_keywords` | (count of Keyword objects) | Total keywords allowed |
### Monthly Limits (Reset on Billing Cycle)
| Limit | Plan Field | Account Field | Description |
|-------|------------|---------------|-------------|
| Ahrefs Queries | `max_ahrefs_queries` | `usage_ahrefs_queries` | Live Ahrefs API queries per month |
### Removed Limits (Now Credit-Based)
The following limits were removed in v1.5.0 - credits handle these:
- ~~max_clusters~~ → Credits
- ~~max_content_ideas~~ → Credits
- ~~max_content_words~~ → Credits
- ~~max_images_basic~~ → Credits
- ~~max_images_premium~~ → Credits
- ~~max_image_prompts~~ → Credits
---
## Plan Tiers
| Plan | Credits/Month | Sites | Users | Keywords | Ahrefs Queries |
|------|---------------|-------|-------|----------|----------------|
| Free | 500 | 1 | 1 | 100 | 0 |
| Starter | 5,000 | 3 | 2 | 500 | 50 |
| Growth | 15,000 | 10 | 5 | 2,000 | 200 |
| Scale | 50,000 | Unlimited | 10 | 10,000 | 500 |
---
## Credit Operations
### Token-Based Operations (Text AI)
Credits calculated from actual token usage:
- `credits = ceil(total_tokens / tokens_per_credit)`
- `tokens_per_credit` defined per model in `AIModelConfig`
| Operation | Model Example | tokens_per_credit |
|-----------|---------------|-------------------|
| Keyword Clustering | gpt-4o-mini | 10,000 |
| Idea Generation | gpt-4o-mini | 10,000 |
| Content Generation | gpt-4o | 1,000 |
| Content Optimization | gpt-4o-mini | 10,000 |
### Fixed-Cost Operations (Image AI) - v1.7.1 Complete
Credits per image based on quality tier:
| Quality Tier | Model Example | Credits/Image |
|--------------|---------------|---------------|
| Basic | runware:97@1 | 1 |
| Quality | dall-e-3 | 5 |
| Premium | google:4@2 | 15 |
**Image Generation Credit Flow (v1.7.1):**
1. Pre-check: `CreditService.check_credits_for_image()` verifies sufficient credits
2. Generation: Images generated via `process_image_generation_queue` Celery task
3. Post-deduct: `CreditService.deduct_credits_for_image()` called per successful image
4. Logging: `CreditUsageLog` + `CreditTransaction` + `AITaskLog` entries created
5. Notifications: `NotificationService.notify_images_complete/failed()` called
### Free Operations
| Operation | Cost |
|-----------|------|
| Add keyword (manual) | 0 |
| Create content task | 0 |
| Edit content | 0 |
| Publish to WordPress | 0 |
| Sync from WordPress | 0 |
---
## Database Models
### Account (Credit Balance)
```python
class Account(models.Model):
credits = models.IntegerField(default=0) # Current balance
usage_ahrefs_queries = models.IntegerField(default=0) # Monthly Ahrefs usage
```
### Plan (Allocations)
```python
class Plan(models.Model):
included_credits = models.IntegerField(default=0) # Monthly allocation
max_sites = models.IntegerField(default=1)
max_users = models.IntegerField(default=1)
max_keywords = models.IntegerField(default=100)
max_ahrefs_queries = models.IntegerField(default=0) # Monthly Ahrefs limit
```
### CreditTransaction (Ledger)
```python
class CreditTransaction(models.Model):
account = models.ForeignKey(Account)
transaction_type = models.CharField() # purchase/subscription/refund/deduction
amount = models.DecimalField() # Positive (add) or negative (deduct)
balance_after = models.DecimalField()
description = models.CharField()
created_at = models.DateTimeField()
```
### CreditUsageLog (Analytics)
```python
class CreditUsageLog(models.Model):
account = models.ForeignKey(Account)
operation_type = models.CharField() # clustering/content_generation/image_generation
credits_used = models.DecimalField()
model_used = models.CharField()
tokens_input = models.IntegerField()
tokens_output = models.IntegerField()
created_at = models.DateTimeField()
```
---
## Business Logic
### CreditService
Location: `backend/igny8_core/business/billing/services/credit_service.py`
**Key Methods:**
```python
class CreditService:
@staticmethod
def check_credits(account, required_credits):
"""Check if sufficient credits available, raises InsufficientCreditsError if not"""
@staticmethod
def deduct_credits_for_operation(account, operation_type, model, tokens_in, tokens_out, metadata=None):
"""Deduct credits and log usage after AI operation"""
@staticmethod
def add_credits(account, amount, transaction_type, description):
"""Add credits (admin/purchase/subscription)"""
@staticmethod
def calculate_credits_from_tokens(operation_type, tokens_in, tokens_out, model=None):
"""Calculate credits based on token usage and model"""
```
### LimitService
Location: `backend/igny8_core/business/billing/services/limit_service.py`
**Key Methods:**
```python
class LimitService:
HARD_LIMIT_MAPPINGS = {
'sites': {...},
'users': {...},
'keywords': {...},
}
MONTHLY_LIMIT_MAPPINGS = {
'ahrefs_queries': {...},
}
@classmethod
def check_hard_limit(cls, account, limit_name, additional_count=1):
"""Check if adding items would exceed hard limit"""
@classmethod
def check_monthly_limit(cls, account, limit_name, additional_count=1):
"""Check if operation would exceed monthly limit"""
@classmethod
def increment_monthly_usage(cls, account, limit_name, count=1):
"""Increment monthly usage counter"""
```
### Usage in AI Operations
```python
# In content generation service
def generate_content(task, user):
account = task.site.account
# 1. Pre-check credits (estimated)
estimated_credits = 50 # Estimate for content generation
CreditService.check_credits(account, estimated_credits)
# 2. Execute AI function
content, usage = ai_engine.generate_content(task)
# 3. Deduct actual credits based on token usage
CreditService.deduct_credits_for_operation(
account=account,
operation_type='content_generation',
model=usage.model,
tokens_in=usage.input_tokens,
tokens_out=usage.output_tokens,
metadata={'content_id': content.id}
)
return content
```
---
## API Responses
### Successful Operation
```json
{
"success": true,
"data": { ... },
"credits_used": 15,
"balance": 9985
}
```
### Insufficient Credits
```json
HTTP 402 Payment Required
{
"success": false,
"error": "Insufficient credits",
"code": "INSUFFICIENT_CREDITS",
"required": 50,
"available": 25
}
```
### Limit Exceeded
```json
HTTP 402 Payment Required
{
"success": false,
"error": "Keyword limit reached",
"code": "HARD_LIMIT_EXCEEDED",
"limit": "keywords",
"current": 500,
"max": 500
}
```
---
## Frontend Handling
### Credit Balance Display
- Header shows current credit balance
- Updates after each operation
- Warning at low balance (< 10%)
### Pre-Operation Check
```typescript
import { checkCreditsBeforeOperation } from '@/utils/creditCheck';
import { useInsufficientCreditsModal } from '@/components/billing/InsufficientCreditsModal';
function ContentGenerator() {
const { showModal } = useInsufficientCreditsModal();
const handleGenerate = async () => {
// Check credits before operation
const check = await checkCreditsBeforeOperation(50); // estimated cost
if (!check.hasEnoughCredits) {
showModal({
requiredCredits: check.requiredCredits,
availableCredits: check.availableCredits,
});
return;
}
// Proceed with generation
await generateContent();
};
}
```
---
## API Endpoints
### Credit Balance
```
GET /api/v1/billing/balance/
```
Response:
```json
{
"credits": 9500,
"plan_credits_per_month": 10000,
"credits_used_this_month": 500,
"credits_remaining": 9500
}
```
### Usage Limits
```
GET /api/v1/billing/usage/limits/
```
Response:
```json
{
"limits": {
"sites": { "current": 2, "limit": 5, "type": "hard" },
"users": { "current": 2, "limit": 3, "type": "hard" },
"keywords": { "current": 847, "limit": 1000, "type": "hard" },
"ahrefs_queries": { "current": 23, "limit": 50, "type": "monthly" }
},
"days_until_reset": 18
}
```
### Usage Analytics
```
GET /api/v1/account/usage/analytics/?days=30
```
Response:
```json
{
"period_days": 30,
"start_date": "2025-12-06",
"end_date": "2026-01-05",
"current_balance": 9500,
"total_usage": 500,
"total_purchases": 0,
"usage_by_type": [
{ "transaction_type": "content_generation", "total": -350, "count": 15 },
{ "transaction_type": "image_generation", "total": -100, "count": 20 },
{ "transaction_type": "clustering", "total": -50, "count": 10 }
],
"daily_usage": [
{ "date": "2026-01-05", "usage": 25, "purchases": 0, "net": -25 }
]
}
```
---
## Credit Allocation
Credits are added to `Account.credits` when:
1. **Subscription Renewal** - `Plan.included_credits` added monthly
2. **Payment Approval** - Manual payments approved by admin
3. **Credit Purchase** - Credit packages bought by user
4. **Admin Adjustment** - Manual credit grants/adjustments
### Monthly Reset
Monthly limits (Ahrefs queries) reset on billing cycle:
```python
# In Account model
def reset_monthly_usage(self):
"""Reset monthly usage counters (called on billing cycle renewal)"""
self.usage_ahrefs_queries = 0
self.save(update_fields=['usage_ahrefs_queries'])
```
---
## Admin Operations
### Manual Credit Adjustment
Via Django Admin or API:
```python
from igny8_core.business.billing.services.credit_service import CreditService
# Add credits
CreditService.add_credits(
account=account,
amount=1000,
transaction_type='adjustment',
description='Customer support adjustment'
)
```
### Usage Audit
All credit changes logged in `CreditTransaction` with:
- Timestamp
- Transaction type
- Amount (positive or negative)
- Balance after transaction
- Description
All AI operations logged in `CreditUsageLog` with:
- Operation type
- Credits used
- Model used
- Token counts
- Related object metadata
---
## Image Generation Credit System (v1.7.1)
### Implementation Details
**Files:**
- `CreditService.check_credits_for_image()` - [credit_service.py:307-335](../backend/igny8_core/business/billing/services/credit_service.py#L307-L335)
- `process_image_generation_queue` credit check - [tasks.py:290-319](../backend/igny8_core/ai/tasks.py#L290-L319)
- `deduct_credits_for_image()` - [tasks.py:745-770](../backend/igny8_core/ai/tasks.py#L745-L770)
- AITaskLog logging - [tasks.py:838-875](../backend/igny8_core/ai/tasks.py#L838-L875)
- Notifications - [tasks.py:877-895](../backend/igny8_core/ai/tasks.py#L877-L895)
### Credit Flow for Image Generation
```
1. User triggers image generation
2. CreditService.check_credits_for_image(account, model, num_images)
- Calculates: credits_per_image × num_images
- Raises InsufficientCreditsError if balance < required
3. process_image_generation_queue() processes each image
4. For each successful image:
CreditService.deduct_credits_for_image()
- Creates CreditUsageLog entry
- Creates CreditTransaction entry
- Updates account.credits balance
5. After all images processed:
- AITaskLog entry created
- Notification created (success or failure)
```
### Logging Locations
| Table | What's Logged | When |
|-------|---------------|------|
| CreditTransaction | Credit deduction (financial ledger) | Per image |
| CreditUsageLog | Usage details (model, cost, credits) | Per image |
| AITaskLog | Task execution summary | After batch |
| Notification | User notification | After batch |
### Automation Compatibility
Image generation credits work identically for:
- Manual image generation (from UI)
- Automation Stage 6 (scheduled/manual automation runs)
Both call `process_image_generation_queue` which handles:
- Credit checking before generation
- Credit deduction after each successful image
- Proper logging to all tables

View File

@@ -0,0 +1,596 @@
# Scheduled Content Publishing Workflow
**Last Updated:** January 20, 2026
**Version:** 1.8.4
**Module:** Publishing / Automation
**Status:** ✅ Site filtering fixed and verified
---
## Overview
IGNY8 provides automated content publishing to WordPress, Shopify, and custom sites. Content goes through a scheduling process before being published at the designated time.
**Current System (v2.0):**
- Site credentials stored directly on `Site` model (`api_key`, `domain`, `platform_type`)
- Multi-platform support: WordPress, Shopify, Custom APIs
- No `SiteIntegration` model required
- Publishing via `PublisherService` (not legacy Celery tasks)
- API endpoint: `POST /api/v1/publisher/publish`
- **Site Filtering:** All content queries filtered by `site_id` (fixed Jan 2026)
---
## Content Lifecycle for Publishing
### Understanding Content.status vs Content.site_status
Content has **TWO separate status fields**:
1. **`status`** - Editorial workflow status
- `draft` - Being created/edited
- `review` - Submitted for review
- `approved` - Ready for publishing
- `published` - Legacy (not used for external publishing)
2. **`site_status`** - External site publishing status (platform-agnostic)
- `not_published` - Not yet published to any site
- `scheduled` - Has a scheduled_publish_at time
- `publishing` - Currently being published
- `published` - Successfully published to site (WordPress, Shopify, or Custom)
- `failed` - Publishing failed
**Note:** `site_status` works across all platforms (WordPress, Shopify, Custom) and is filtered by `site_id` to show only content for the selected site.
### Publishing Flow
```
┌─────────────────┐
│ DRAFT │ ← Content is being created/edited
│ status: draft │
└────────┬────────┘
│ User approves content
┌─────────────────┐
│ APPROVED │ ← Content is ready for publishing
│ status: approved│ status='approved', site_status='not_published'
│ site_status: │
│ not_published │
└────────┬────────┘
│ Hourly: schedule_approved_content task
┌─────────────────┐
│ SCHEDULED │ ← Content has a scheduled_publish_at time
│ status: approved│ site_status='scheduled'
│ site_status: │ scheduled_publish_at set to future datetime
│ scheduled │
└────────┬────────┘
│ Every 5 min: process_scheduled_publications task
│ (when scheduled_publish_at <= now)
┌─────────────────┐
│ PUBLISHING │ ← WordPress API call in progress
│ status: approved│ site_status='publishing'
│ site_status: │
│ publishing │
└────────┬────────┘
┌────┴────┐
│ │
▼ ▼
┌────────┐ ┌────────┐
│PUBLISHED│ │ FAILED │
│status: │ │status: │
│approved │ │approved│
│site_ │ │site_ │
│status: │ │status: │
│published│ │failed │
└─────────┘ └────────┘
```
---
## Frontend Integration
### Site Selector & Content Filtering
**Content Calendar** (`frontend/src/pages/Publisher/ContentCalendar.tsx`):
- Automatically filters all content by selected site
- Reloads data when site selector changes
- Shows scheduled, publishing, published, and failed content for active site only
**Critical Implementation Details:**
- All API queries include `site_id: activeSite.id` parameter
- Backend `ContentFilter` includes `site_id` in filterable fields
- useEffect hook reacts to `activeSite?.id` changes to trigger reload
- Content cleared when no site selected
**Site Selector Fix (Jan 2026):**
- Fixed circular dependency in useEffect (lines 285-294)
- Only depends on `activeSite?.id`, not on callback functions
- Added console logging for debugging site changes
- Pattern follows Dashboard and Approved pages
**Backend Filter Configuration:**
```python
# backend/igny8_core/modules/writer/views.py
class ContentFilter(django_filters.FilterSet):
class Meta:
model = Content
fields = [
'cluster_id',
'site_id', # Required for site filtering
'status',
'site_status', # Required for status filtering
'content_type',
# ...
]
```
---
## Celery Tasks
### 1. `schedule_approved_content`
**Schedule:** Every hour at :00
**Task Name:** `publishing.schedule_approved_content`
**File:** `backend/igny8_core/tasks/publishing_scheduler.py`
#### What It Does:
1. Finds all sites with `PublishingSettings.auto_publish_enabled = True`
2. Gets approved content (`status='approved'`, `site_status='not_published'`, `scheduled_publish_at=null`)
3. Calculates available publishing slots based on:
- `publish_days` - which days are allowed (e.g., Mon-Fri)
- `publish_time_slots` - which times are allowed (e.g., 09:00, 14:00, 18:00)
- `daily_publish_limit` - max posts per day
- `weekly_publish_limit` - max posts per week
- `monthly_publish_limit` - max posts per month
4. Assigns `scheduled_publish_at` datetime and sets `site_status='scheduled'`
#### Configuration Location:
`PublishingSettings` model linked to each Site. Configurable via:
- Admin: `/admin/integration/publishingsettings/`
- API: `/api/v1/sites/{site_id}/publishing-settings/`
---
### 2. `process_scheduled_publications`
**Schedule:** Every 5 minutes
**Task Name:** `publishing.process_scheduled_publications`
**File:** `backend/igny8_core/tasks/publishing_scheduler.py`
#### What It Does:
1. Finds all content where:
- `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'`
- 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. `PublisherService.publish_content`
**Type:** Service method (called by scheduler)
**File:** `backend/igny8_core/business/publishing/services/publisher_service.py`
#### What It Does:
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.domain}/wp-json/igny8/v1/publish`
- Authentication: API key from `Site.wp_api_key`
- **No SiteIntegration model needed**
---
## Database Models
### Content Fields (Publishing Related)
| Field | Type | Description |
|-------|------|-------------|
| `status` | CharField | **Editorial workflow**: `draft`, `review`, `approved` |
| `site_status` | CharField | **WordPress publishing status**: `not_published`, `scheduled`, `publishing`, `published`, `failed` |
| `site_status_updated_at` | DateTimeField | When site_status was last changed |
| `scheduled_publish_at` | DateTimeField | When content should be published (null if not scheduled) |
| `external_id` | CharField | WordPress post ID after publishing |
| `external_url` | URLField | WordPress post URL after publishing |
**Important:** These are separate concerns:
- `status` tracks editorial approval
- `site_status` tracks external publishing
- Content typically has `status='approved'` AND `site_status='not_published'` before scheduling
### PublishingSettings Fields
| Field | Type | Description |
|-------|------|-------------|
| `site` | ForeignKey | The site these settings apply to |
| `auto_publish_enabled` | BooleanField | Whether automatic scheduling is enabled |
| `publish_days` | JSONField | List of allowed days: `['mon', 'tue', 'wed', 'thu', 'fri']` |
| `publish_time_slots` | JSONField | List of times: `['09:00', '14:00', '18:00']` |
| `daily_publish_limit` | IntegerField | Max posts per day (null = unlimited) |
| `weekly_publish_limit` | IntegerField | Max posts per week (null = unlimited) |
| `monthly_publish_limit` | IntegerField | Max posts per month (null = unlimited) |
---
## Celery Beat Schedule
From `backend/igny8_core/celery.py`:
```python
app.conf.beat_schedule = {
# ...
'schedule-approved-content': {
'task': 'publishing.schedule_approved_content',
'schedule': crontab(minute=0), # Every hour at :00
},
'process-scheduled-publications': {
'task': 'publishing.process_scheduled_publications',
'schedule': crontab(minute='*/5'), # Every 5 minutes
},
# ...
}
```
---
## Manual Publishing
Content can be published immediately or rescheduled via API:
### Publish Now
```
POST /api/v1/publisher/publish
{
"content_id": 123,
"destinations": ["wordpress"]
}
```
### 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/
```
---
## Monitoring & Debugging
### Frontend Debugging
**Browser Console Logs:**
When changing sites in Content Calendar, you should see:
```
[ContentCalendar] Site changed to: 45 My Site Name
[ContentCalendar] Triggering loadQueue...
```
**Check Site Filtering:**
1. Open browser DevTools → Network tab
2. Change site in site selector
3. Look for API calls to `/api/v1/writer/content/`
4. Verify `site_id` parameter is included in query string
5. Verify count matches database for that site
**Common Issues:**
- No console logs when changing sites → useEffect not triggering (refresh page)
- API calls missing `site_id` parameter → backend filter not working
- Wrong count displayed → database query issue or cache problem
### Backend Log Files
- **Publish Logs:** `backend/logs/publish-sync-logs/`
- **API Logs:** `backend/logs/wordpress_api.log`
### Check Celery Status
```bash
docker compose -f docker-compose.app.yml -p igny8-app logs igny8_celery_worker
docker compose -f docker-compose.app.yml -p igny8-app logs igny8_celery_beat
```
### Check Scheduled Content
```python
# Django shell
from igny8_core.business.content.models import Content
from django.utils import timezone
# Past due content (should have been published)
Content.objects.filter(
site_status='scheduled',
scheduled_publish_at__lt=timezone.now()
).count()
# Upcoming scheduled content
Content.objects.filter(
site_status='scheduled',
scheduled_publish_at__gt=timezone.now()
).order_by('scheduled_publish_at')[:10]
# Check scheduled content for specific site
site_id = 45
Content.objects.filter(
site_id=site_id,
site_status='scheduled'
).count()
# Compare with frontend display
# Should match count shown in Content Calendar for that site
```
### Manual Task Execution
```python
# Django shell
from igny8_core.tasks.publishing_scheduler import (
schedule_approved_content,
process_scheduled_publications
)
# Run scheduling task
schedule_approved_content()
# Process due publications
process_scheduled_publications()
```
---
## Error Handling
### Common Failure Reasons
| Error | Cause | Solution |
|-------|-------|----------|
| 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 | Use reschedule to republish |
### Retry Policy
- 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'])
```
---
## Troubleshooting
### Site Selector Not Updating Content Calendar
**Symptoms:**
- Changing sites doesn't reload calendar content
- Count shows wrong number of scheduled items
- Content from wrong site displayed
**Solution:**
1. **Hard refresh browser:** Ctrl+F5 (Windows/Linux) or Cmd+Shift+R (Mac)
2. **Clear browser cache**
3. **Check console logs:** Should see `[ContentCalendar] Site changed to: ...`
4. **Verify API calls:** Check Network tab for `site_id` parameter
**What Was Fixed (Jan 2026):**
- Frontend: Fixed useEffect circular dependency in ContentCalendar.tsx
- Backend: Added `site_id` and `site_status` to ContentFilter fields
- All API queries now properly filter by `site_id: activeSite.id`
- Content clears when no site selected
**If Problem Persists:**
```bash
# Check backend filter configuration
grep -n "site_id" backend/igny8_core/modules/writer/views.py
# Should show site_id in ContentFilter.Meta.fields
```
### Content Not Being Scheduled
1. Check `PublishingSettings.auto_publish_enabled` is `True`
2. Verify content has `status='approved'` and `site_status='not_published'`
3. Check `scheduled_publish_at` is null (already scheduled content won't reschedule)
4. Verify publish limits haven't been reached
5. **Verify correct site selected** in site selector
### Content Not Publishing
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 Site has `wp_api_key` configured
5. Verify Site has `domain` configured
6. Test WordPress API connectivity
### Resetting Failed Content
Use the reschedule API endpoint:
```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}")
```
---
## Architecture Diagram
```
┌─────────────────────────────────────────────────────────────────┐
│ IGNY8 Backend │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────Validate Site.wp_api_key & domain │ │
│ │ - Call PublisherService.publish_content() │ │
│ │ │ │
│ │ 3. PublisherService │ │
│ │ - Get config from Site model │ │
│ │ - Call WordPressAdapter │ │
│ │ - Update content status │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
└───────────────────────────────────┼─────────────────────────────┘
▼ HTTPS
┌───────────────────────────────────────────────────────────────┐
│ WordPress Site │
├───────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 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
- Handles images, categories, tags
- Returns post ID and URL
```
---
## Related Documentation
- [Publisher Module](../10-MODULES/PUBLISHER.md)
- [WordPress Integration](../60-PLUGINS/WORDPRESS-INTEGRATION.md)
- [Content Pipeline](CONTENT-PIPELINE.md)