temproary docs uplaoded
This commit is contained in:
@@ -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"
|
||||
```
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user