This commit is contained in:
IGNY8 VPS (Salman)
2026-01-11 16:58:57 +00:00
parent e9369df151
commit 75e5b148f5
5 changed files with 59 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,334 @@
# Footer Widgets Audit - Complete Analysis
**Date:** January 10, 2026
**Purpose:** Document all footer widgets across Planner and Writer pages to identify data conflicts
---
## SUMMARY
All Planner and Writer pages use `StandardThreeWidgetFooter` component which displays:
1. **Widget 1 (Left)**: Page Progress - page-specific metrics
2. **Widget 2 (Middle)**: Module Stats - uses `StandardizedModuleWidget`
3. **Widget 3 (Right)**: Workflow Completion - uses `WorkflowCompletionWidget` via `useWorkflowStats` hook
---
## PLANNER MODULE PAGES
### Page 1: Keywords (`/planner/keywords`)
#### Widget 1: Page Progress
| Field | Value | Source | Filter/Criteria |
|-------|-------|--------|-----------------|
| Keywords | `totalCount` | Local state (line 49) | All keywords for site+sector on current page |
| Clustered | `totalClustered` | Local state (line 50) | Keywords with status='mapped' |
| Unmapped | `totalUnmapped` | Local state (line 51) | Keywords without cluster_id |
| Volume | `totalVolume` | Calculated from keywords | Sum of search volumes |
| Progress % | Calculated | `(totalClustered / totalCount) * 100` | - |
**Data Loading:** Lines 132-183
- Loads keywords via `fetchKeywords({ site_id, sector_id, page, page_size, ...filters })`
- **SECTOR FILTERED**: Yes - uses `activeSector.id`
- Calculates totals from loaded data
- **Issue**: Only calculates from CURRENT PAGE data, not all keywords
#### Widget 2: Module Stats
| Field | Value | Source |
|-------|-------|--------|
| Type | "planner" | Hardcoded prop |
| Component | StandardizedModuleWidget | Centralized component |
#### Widget 3: Workflow Completion
Uses `useWorkflowStats` hook
| Field | API Endpoint | Filter |
|-------|--------------|--------|
| Keywords Total | `/v1/planner/keywords/` | `site_id` only (NO sector) |
| Keywords Clustered | `/v1/planner/keywords/?status=mapped` | `site_id` only |
| Clusters Created | `/v1/planner/clusters/` | `site_id` only |
| Ideas Generated | `/v1/planner/ideas/` | `site_id` only |
| Content Drafts | `/v1/writer/content/?status=draft` | `site_id` only |
| Content Review | `/v1/writer/content/?status=review` | `site_id` only |
| Content Published | `/v1/writer/content/?status__in=approved,published` | `site_id` only |
| Images Created | `/v1/writer/images/` | `site_id` only |
**Source:** `frontend/src/hooks/useWorkflowStats.ts` (lines 144-234)
- **SECTOR FILTERED**: No - intentionally site-wide for consistency
- **Date Filtered**: Yes - supports Today, 7d, 30d, 90d, all
---
### Page 2: Clusters (`/planner/clusters`)
#### Widget 1: Page Progress
| Field | Value | Source | Filter/Criteria |
|-------|-------|--------|-----------------|
| Clusters | `totalCount` | Local state (line 46) | All clusters for site+sector |
| With Ideas | `totalWithIdeas` | Local state (line 47) | Clusters with ideas_count > 0 |
| Keywords | Calculated | Sum of all clusters' keywords_count | From loaded clusters |
| Ready | `totalReady` | Local state (line 48) | Clusters with ideas_count === 0 |
| Progress % | Calculated | `(totalWithIdeas / totalCount) * 100` | - |
**Data Loading:** Lines 94-127
- Loads clusters via `fetchClusters({ site_id, sector_id, page, page_size, ...filters })`
- **SECTOR FILTERED**: Yes - uses `activeSector.id`
- Calculates totals from loaded data
- **Issue**: Only calculates from CURRENT PAGE data
#### Widget 2 & 3: Same as Keywords page
---
### Page 3: Ideas (`/planner/ideas`)
#### Widget 1: Page Progress
| Field | Value | Source | Filter/Criteria |
|-------|-------|--------|-----------------|
| Ideas | `totalCount` | Local state (line 45) | All ideas for site+sector |
| In Tasks | `totalInTasks` | Local state (line 46) | Ideas with task_id not null |
| Pending | `totalPending` | Local state (line 47) | Ideas without task_id |
| From Clusters | `clusters.length` | Loaded clusters count | Unique clusters |
| Progress % | Calculated | `(totalInTasks / totalCount) * 100` | - |
**Data Loading:** Lines 87-133
- Loads ideas via `fetchContentIdeas({ site_id, sector_id, page, page_size, ...filters })`
- **SECTOR FILTERED**: Yes - uses `activeSector.id`
- Loads clusters separately
- **Issue**: Only calculates from CURRENT PAGE data
#### Widget 2 & 3: Same as above
---
## WRITER MODULE PAGES
### Page 4: Tasks (`/writer/tasks`)
#### Widget 1: Page Progress
| Field | Value | Source | Filter/Criteria |
|-------|-------|--------|-----------------|
| Tasks | `totalCount` | Local state (line 47) | All tasks for site+sector |
| Drafted | `totalDrafted` | Local state (line 48) | Tasks with content created |
| Pending | `totalPending` | Local state (line 49) | Tasks without content |
| Priority | `totalPriority` | Local state (line 50) | Tasks with priority=true |
| Progress % | Calculated | `(totalDrafted / totalCount) * 100` | - |
**Data Loading:** Lines 139-182
- Loads tasks via `fetchTasks({ site_id, sector_id, page, page_size, ...filters })`
- **SECTOR FILTERED**: Yes - uses `activeSector.id`
- Calculates totals from loaded data
- **Issue**: Only calculates from CURRENT PAGE data
#### Widget 2: Module Stats
| Field | Value | Source |
|-------|-------|--------|
| Type | "writer" | Hardcoded prop |
| Component | StandardizedModuleWidget | Centralized component |
#### Widget 3: Workflow Completion
Same as Planner pages - uses `useWorkflowStats` hook
---
### Page 5: Content/Drafts (`/writer/content`)
#### Widget 1: Page Progress
| Field | Value | Source | Filter/Criteria |
|-------|-------|--------|-----------------|
| Content | `totalCount` | Local state (line 43) | All content for site+sector |
| Published | `totalPublished` | Local state (line 44) | Content with site_status='published' |
| In Review | `totalInReview` | Local state (line 45) | Content with status='review' |
| Approved | `totalApproved` | Local state (line 46) | Content with status='approved' |
| Progress % | Calculated | `(totalPublished / totalCount) * 100` | - |
**Data Loading:** Lines 84-112
- Loads content via `fetchContent({ site_id, sector_id, page, page_size, ...filters })`
- **SECTOR FILTERED**: Yes - uses `activeSector.id`
- **Issue**: Only calculates from CURRENT PAGE data
#### Widget 2 & 3: Same as Tasks page
---
### Page 6: Review (`/writer/review`)
#### Widget 1: Page Progress
| Field | Value | Source | Filter/Criteria |
|-------|-------|--------|-----------------|
| In Review | `totalCount` | Local state (line 39) | Content with status='review' |
| Approved | `totalApproved` | Local state (line 40) | From review that moved to approved |
| Pending | `totalPending` | Local state (line 41) | Still in review status |
| Priority | `totalPriority` | Local state (line 42) | With priority flag |
| Progress % | Calculated | `(totalApproved / totalCount) * 100` | - |
**Data Loading:** Lines 77-105
- Loads review content via `fetchContent({ site_id, sector_id, status: 'review', page, page_size })`
- **SECTOR FILTERED**: Yes - uses `activeSector.id`
- **Pre-filtered**: Only loads status='review'
- **Issue**: Only calculates from CURRENT PAGE data
#### Widget 2 & 3: Same as other Writer pages
---
### Page 7: Approved (`/writer/approved`)
#### Widget 1: Page Progress
| Field | Value | Source | Filter/Criteria |
|-------|-------|--------|-----------------|
| Approved | `totalCount` | Local state (line 39) | Content with status='approved' |
| Published | `totalPublished` | Local state (line 40) | With site_status='published' |
| Scheduled | `totalScheduled` | Local state (line 41) | With site_status='scheduled' |
| Ready | `totalReady` | Local state (line 42) | With site_status='not_published' |
| Progress % | Calculated | `(totalPublished / totalCount) * 100` | - |
**Data Loading:** Lines 77-107
- Loads approved content via `fetchContent({ site_id, sector_id, status: 'approved', page, page_size })`
- **SECTOR FILTERED**: Yes - uses `activeSector.id`
- **Pre-filtered**: Only loads status='approved'
- **Issue**: Only calculates from CURRENT PAGE data
#### Widget 2 & 3: Same as other Writer pages
---
### Page 8: Images (`/writer/images`)
#### Widget 1: Page Progress
| Field | Value | Source | Filter/Criteria |
|-------|-------|--------|-----------------|
| Images | `totalCount` | Local state (line 44) | All images for site+sector |
| Featured | `totalFeatured` | Local state (line 45) | Images with image_type='featured' |
| In-Article | `totalInArticle` | Local state (line 46) | Images with image_type='in_article' |
| Linked | `totalLinked` | Local state (line 47) | Images with content_id not null |
| Progress % | Calculated | `(totalLinked / totalCount) * 100` | - |
**Data Loading:** Lines 98-144
- Loads images via `fetchImages({ site_id, sector_id, page, page_size, ...filters })`
- **SECTOR FILTERED**: Yes - uses `activeSector.id`
- **Issue**: Only calculates from CURRENT PAGE data
#### Widget 2 & 3: Same as other Writer pages
---
## ROOT CAUSES OF DATA CONFLICTS
### Problem 1: Page-Level vs Site-Wide Data
**Conflict:** Widget 1 (Page Progress) shows **page-filtered** counts, Widget 3 (Workflow) shows **site-wide** counts
| Widget | Scope | Sector Filter | Date Filter | Data Source |
|--------|-------|---------------|-------------|-------------|
| Widget 1 (Page Progress) | Current page only | YES | NO | Local state from paginated API |
| Widget 2 (Module Stats) | Site-wide | NO | NO | Centralized hook (StandardizedModuleWidget) |
| Widget 3 (Workflow) | Site-wide | NO | YES (optional) | useWorkflowStats hook |
**Example Conflict:**
- Keywords page shows "17 Keywords" in Page Progress (Widget 1) ← from current page
- Workflow widget shows "17 Keywords Clustered" (Widget 3) ← from ALL keywords site-wide
- If user is on page 2, Widget 1 shows page 2 keywords, but Widget 3 shows total site keywords
### Problem 2: Sector Filtering Inconsistency
**Conflict:** Widget 1 filters by sector, Widget 3 does NOT
| Component | Sector Filtered? | Reasoning |
|-----------|------------------|-----------|
| Page Progress (Widget 1) | ✅ YES | Shows current page data which is sector-filtered |
| Module Stats (Widget 2) | ❌ NO | Centralized module-level stats |
| Workflow Widget (Widget 3) | ❌ NO | Intentionally site-wide for consistency across pages |
**User's Point:** If only 1 sector exists, sector filter doesn't matter - but data STILL conflicts because Widget 1 shows PAGINATED data
### Problem 3: Pagination vs Total Counts
**Critical Issue:** Widget 1 calculates totals from **current page data only**, not all records
Example on Keywords page (lines 182-183):
```typescript
setTotalCount(keywords.length); // ← Only current page!
setTotalClustered(keywords.filter(k => k.status === 'mapped').length); // ← Only current page!
```
Should be:
```typescript
setTotalCount(response.count); // ← Total from API
setTotalClustered(/* need separate API call or response field */);
```
### Problem 4: Different Time Ranges
| Widget | Time Filtering |
|--------|----------------|
| Widget 1 | NO time filter - shows ALL data for site+sector |
| Widget 3 | YES time filter - supports Today, 7d, 30d, 90d buttons |
---
## RECOMMENDED FIXES
### Fix 1: Make Page Progress Show Site-Wide Totals
**Current:** Calculates from paginated data
**Should Be:** Use `response.count` from API for totals
**Files to Fix:**
- `frontend/src/pages/Planner/Keywords.tsx`
- `frontend/src/pages/Planner/Clusters.tsx`
- `frontend/src/pages/Planner/Ideas.tsx`
- `frontend/src/pages/Writer/Tasks.tsx`
- `frontend/src/pages/Writer/Content.tsx`
- `frontend/src/pages/Writer/Review.tsx`
- `frontend/src/pages/Writer/Approved.tsx`
- `frontend/src/pages/Writer/Images.tsx`
**Change Pattern:**
```typescript
// OLD (WRONG):
setTotalCount(items.length); // Only current page
// NEW (CORRECT):
setTotalCount(response.count); // Total count from API
```
### Fix 2: Document That Widgets Show Different Scopes
**Add tooltips/help text:**
- Widget 1: "Page statistics (current filters)"
- Widget 3: "Site-wide workflow progress (all sectors)"
### Fix 3: Consider Adding Sector Filter Option to Widget 3
**Alternative:** Add toggle in Workflow Widget to switch between:
- Site-wide (current behavior)
- Current sector only (match Widget 1)
---
## ADDITIONAL FINDINGS
### Publishing Tab Issues
**File:** `frontend/src/pages/Sites/Settings.tsx`
**Issue:** Day selection and time slot changes auto-save immediately instead of waiting for "Save Publishing Settings" button
**Lines with auto-save:**
- Line 1195: Publishing days button click calls `savePublishingSettings({ publish_days: newDays })`
- Line 1224: Remove time slot calls `savePublishingSettings({ publish_time_slots: newSlots })`
- Line 1236: Add time slot calls `savePublishingSettings({ publish_time_slots: newSlots })`
**Fix:** Remove `savePublishingSettings()` calls from these onChange handlers, let user click the Save button at line 1278
---
## SUMMARY TABLE: ALL PAGES
| Page | Widget 1 Scope | Widget 1 Sector Filter | Widget 3 Scope | Widget 3 Sector Filter | Conflict? |
|------|---------------|----------------------|---------------|----------------------|-----------|
| Keywords | Current page | YES | Site-wide | NO | ✅ YES |
| Clusters | Current page | YES | Site-wide | NO | ✅ YES |
| Ideas | Current page | YES | Site-wide | NO | ✅ YES |
| Tasks | Current page | YES | Site-wide | NO | ✅ YES |
| Content | Current page | YES | Site-wide | NO | ✅ YES |
| Review | Current page | YES | Site-wide | NO | ✅ YES |
| Approved | Current page | YES | Site-wide | NO | ✅ YES |
| Images | Current page | YES | Site-wide | NO | ✅ YES |
**Conclusion:** ALL pages have the pagination vs site-wide conflict. The sector filtering is actually a secondary issue.
---
## END OF AUDIT

View File

@@ -0,0 +1,373 @@
# Image Generation System - Comprehensive Gap Analysis
**Date:** January 2026
**Status:** Audit Complete
**Reviewer:** System Audit
## Executive Summary
This document provides a comprehensive audit of the image generation system, analyzing the flow from model configuration to image delivery, both for manual and automation workflows.
---
## 1. System Architecture Overview
### Current Flow
```
User Selects Quality Tier (basic/quality/quality_option2/premium)
AIModelConfig (database) → provider, model_name, landscape_size, square_size
process_image_generation_queue (Celery task)
ai_core.generate_image() → provider-specific handler
_generate_image_openai() / _generate_image_runware()
Downloaded to /frontend/public/images/ai-images/
Image record updated (Images model)
```
### Image Count Determination
```
User sets max_images (1-8) in Site Settings
AISettings.get_effective_max_images(account)
generate_image_prompts.py: 1 featured + max_images in_article
Images created with positions: featured(0) + in_article(0,1,2,3...)
```
---
## 2. RESOLVED Issues (Previously Fixed)
### ✅ 2.1 Quality Tier Fallback
- **Issue:** `get_effective_quality_tier()` was returning hardcoded 'basic' instead of default model's tier
- **Fixed in:** `ai_settings.py` lines 196-232
- **Solution:** Now falls back to `AIModelConfig.is_default=True` model's `quality_tier`
### ✅ 2.2 Image Sizes from Database
- **Issue:** `tasks.py` had hardcoded `MODEL_LANDSCAPE_SIZES` dict
- **Fixed in:** `tasks.py` lines 242-260
- **Solution:** Now loads `landscape_size` and `square_size` from `AIModelConfig`
### ✅ 2.3 Settings Endpoint Default Tier
- **Issue:** `settings_views.py` returned hardcoded 'basic' as default tier
- **Fixed in:** `settings_views.py` lines 616-640
- **Solution:** Gets `default_tier` from `default_image_model.quality_tier`
### ✅ 2.4 Integration Views Dynamic Sizes
- **Issue:** Two endpoints in `integration_views.py` had hardcoded size lookup
- **Fixed:** Both endpoints now load from `AIModelConfig`
---
## 3. REMAINING Gaps (Action Required)
### 🔴 GAP-1: Hardcoded Size Constants in global_settings_models.py
**Location:** `backend/igny8_core/modules/system/global_settings_models.py` lines 180-188
**Code:**
```python
# Model-specific landscape sizes (square is always 1024x1024 for all models)
MODEL_LANDSCAPE_SIZES = {
'runware:97@1': '1280x768', # Hi Dream Full landscape
'bria:10@1': '1344x768', # Bria 3.2 landscape (16:9)
'google:4@2': '1376x768', # Nano Banana landscape (16:9)
}
# Default square size (universal across all models)
DEFAULT_SQUARE_SIZE = '1024x1024'
```
**Impact:** These constants are UNUSED but could cause confusion. They're legacy from before the AIModelConfig migration.
**Recommendation:**
- [ ] Remove unused `MODEL_LANDSCAPE_SIZES` dict
- [ ] Remove unused `DEFAULT_SQUARE_SIZE` constant
- [ ] Add deprecation comment if keeping for reference
---
### 🔴 GAP-2: Hardcoded Size Fallbacks in Tasks
**Location:** `backend/igny8_core/ai/tasks.py` lines 254-260
**Code:**
```python
else:
# Fallback sizes if no model config (should never happen)
model_landscape_size = '1792x1024'
model_square_size = '1024x1024'
logger.warning(f"[process_image_generation_queue] No model config, using fallback sizes")
```
**Impact:** LOW - Fallback only triggers if database is misconfigured. But the hardcoded sizes may not match actual model requirements.
**Recommendation:**
- [ ] Consider failing gracefully with clear error instead of using fallback
- [ ] Or: Load fallback from a system default in database
---
### 🔴 GAP-3: Frontend VALID_SIZES_BY_MODEL Hardcoded
**Location:** `frontend/src/components/common/ImageGenerationCard.tsx` lines 52-55
**Code:**
```tsx
const VALID_SIZES_BY_MODEL: Record<string, string[]> = {
'dall-e-3': ['1024x1024', '1024x1792', '1792x1024'],
'dall-e-2': ['256x256', '512x512', '1024x1024'],
};
```
**Impact:** MEDIUM - Test image generation card only shows OpenAI sizes, not Runware/Bytedance sizes.
**Recommendation:**
- [ ] Fetch valid_sizes from AIModelConfig via API
- [ ] Or: Pass sizes from backend settings endpoint
---
### 🔴 GAP-4: Backend VALID_SIZES_BY_MODEL Hardcoded
**Location:** `backend/igny8_core/ai/constants.py` lines 40-43
**Code:**
```python
VALID_SIZES_BY_MODEL = {
'dall-e-3': ['1024x1024', '1024x1792', '1792x1024'],
'dall-e-2': ['256x256', '512x512', '1024x1024'],
}
```
**Impact:** MEDIUM - Used for OpenAI validation only. Runware models bypass this validation.
**Status:** PARTIAL - Only affects OpenAI validation. Runware has its own validation via provider-specific code.
**Recommendation:**
- [ ] Move validation to AIModelConfig.valid_sizes field
- [ ] Validate against model's valid_sizes from database
---
### 🔴 GAP-5: Missing Runware Model Size Validation
**Location:** `backend/igny8_core/ai/ai_core.py` lines 943-1050
**Code:** The `_generate_image_runware()` method does NOT validate sizes against `valid_sizes` from database.
**Impact:** LOW - Runware API will reject invalid sizes anyway, but error message won't be clear.
**Recommendation:**
- [ ] Add validation: check `size` against `AIModelConfig.valid_sizes` before API call
- [ ] Return clear error: "Size X is not valid for model Y"
---
### 🔴 GAP-6: Seedream Minimum Pixel Validation Hardcoded
**Location:** `backend/igny8_core/ai/ai_core.py` lines 1018-1027
**Code:**
```python
elif runware_model.startswith('bytedance:'):
# Enforce minimum size for Seedream (min 3,686,400 pixels ~ 1920x1920)
current_pixels = width * height
if current_pixels < 3686400:
# Use default Seedream square size
inference_task['width'] = 2048
inference_task['height'] = 2048
```
**Impact:** LOW - This hardcoded check works, but should come from database.
**Recommendation:**
- [ ] Add `min_pixels` field to AIModelConfig
- [ ] Check model.min_pixels instead of hardcoded 3686400
---
### 🔴 GAP-7: Provider-Specific Steps/CFGScale Hardcoded
**Location:** `backend/igny8_core/ai/ai_core.py` lines 995-1040
**Code:**
```python
if runware_model.startswith('bria:'):
inference_task['steps'] = 20
# ...
elif runware_model.startswith('runware:'):
inference_task['steps'] = 20
inference_task['CFGScale'] = 7
```
**Impact:** LOW - Works correctly but adding new models requires code changes.
**Recommendation:**
- [ ] Add `generation_params` JSON field to AIModelConfig
- [ ] Store steps, CFGScale, etc. in database per model
---
### 🔴 GAP-8: Image Count Not Per-Content Configurable
**Location:** System-wide setting only
**Current Behavior:**
- `max_images` is a global setting (site-wide)
- All articles get the same number of in_article images
**Impact:** MEDIUM - Users cannot set different image counts per article/keyword.
**Recommendation:**
- [ ] Add `images_count` field to Content model (nullable, inherits from site default)
- [ ] Or: Add to Keywords model for keyword-level override
---
### 🟡 GAP-9: Legacy generate_images.py Function (Partially Dead Code)
**Location:** `backend/igny8_core/ai/functions/generate_images.py`
**Issue:** `generate_images_core()` function exists but appears to be legacy. Main flow uses `process_image_generation_queue()` in tasks.py.
**Impact:** LOW - Code duplication, potential maintenance burden.
**Recommendation:**
- [ ] Audit if `generate_images.py` is actually used anywhere
- [ ] If not: Add deprecation warning or remove
- [ ] If used: Ensure it uses same dynamic config loading
---
### 🟡 GAP-10: No Validation of quality_tier Values
**Location:** Multiple locations
**Issue:** When user selects a quality_tier, there's no validation that:
1. The tier exists in AIModelConfig
2. The tier has an active model
3. The user's plan allows that tier
**Impact:** MEDIUM - Could lead to runtime errors if tier doesn't exist.
**Recommendation:**
- [ ] Add validation in settings save endpoint
- [ ] Return error if selected tier has no active model
---
## 4. Image Count Flow (Working Correctly)
### How image count works:
1. **User configures:**
- `max_images` (1-8) in Site Settings → saved to AccountSettings
2. **Prompt generation:**
```python
# generate_image_prompts.py
max_images = AISettings.get_effective_max_images(account) # e.g., 4
# Creates: 1 featured + 4 in_article = 5 image prompts
```
3. **Image types:**
- `featured` - Always 1, position=0, landscape size
- `in_article` - Up to max_images, positions 0,1,2,3..., alternating square/landscape
4. **Size determination:**
```python
# tasks.py
if image.image_type == 'featured':
image_size = featured_image_size # landscape
elif image.image_type == 'in_article':
position = image.position or 0
if position % 2 == 0: # 0, 2
image_size = square_size
else: # 1, 3
image_size = landscape_size
```
**STATUS:** ✅ Working correctly. No gaps identified in image count logic.
---
## 5. Automation Flow (Working Correctly)
### Stage 6: Image Generation
**Location:** `automation_service.py` lines 1236-1400
**Flow:**
1. Query `Images.objects.filter(site=site, status='pending')`
2. For each image: `process_image_generation_queue.delay(image_ids=[image.id], ...)`
3. Monitor task completion
4. Update run progress
**STATUS:** ✅ Uses same task as manual generation. Consistent behavior.
---
## 6. Model Provider Support Matrix
| Provider | Models | Status | Gaps |
|----------|--------|--------|------|
| OpenAI | dall-e-3, dall-e-2 | ✅ Working | Valid sizes hardcoded |
| Runware | runware:97@1 | ✅ Working | No size validation |
| Runware | bria:10@1 | ✅ Working | Steps hardcoded |
| Runware | google:4@2 | ✅ Working | Resolution param hardcoded |
| Runware | bytedance:seedream@4.5 | ✅ Working | Min pixels hardcoded |
---
## 7. Priority Action Items
### High Priority
1. **GAP-4/5:** Implement database-driven size validation for all providers
2. **GAP-10:** Add quality_tier validation on save
### Medium Priority
3. **GAP-6/7:** Move provider-specific params to AIModelConfig.generation_params
4. **GAP-8:** Consider per-content image count override
### Low Priority
5. **GAP-1:** Clean up unused constants
6. **GAP-9:** Audit and deprecate legacy code
7. **GAP-3:** Fetch valid sizes from API in frontend
---
## 8. Recommendations Summary
### Short-term (Before Launch)
- Ensure all hardcoded fallbacks are clearly logged
- Test each model tier end-to-end
### Medium-term (Post-Launch)
- Migrate all hardcoded params to AIModelConfig fields
- Add model validation on quality_tier save
### Long-term (Future Enhancement)
- Per-content image count override
- Per-keyword image style override
- Image regeneration without deleting existing
---
## Appendix: Key Files Reference
| File | Purpose |
|------|---------|
| `ai/tasks.py` | Main image generation Celery task |
| `ai/ai_core.py` | Provider-specific generation methods |
| `ai/functions/generate_image_prompts.py` | Extract prompts from content |
| `modules/system/ai_settings.py` | System defaults + account overrides |
| `modules/system/settings_views.py` | Frontend settings API |
| `business/billing/models.py` | AIModelConfig model |
| `business/automation/services/automation_service.py` | Automation Stage 6 |

View File

@@ -0,0 +1,814 @@
---
## 🎨 DESIGN ANALYSIS & PLAN
### Current State Analysis
**WordPress Single Post Template:**
- Uses emojis (📅, 📝, ✍️, 📁, 🏷️) in header metadata
- Shows all metadata including internal/debug data (Content ID, Task ID, Sector ID, Cluster ID, etc.)
- SEO section shows Meta Title & Meta Description in header (should be hidden from users)
- "Section Spotlight" label is hardcoded
- Images not optimally distributed - one per section sequentially
- Container max-width: 1200px
**App ContentViewTemplate:**
- Uses icons properly via component imports
- Similar "Section Spotlight" label issue
- Better image handling with aspect ratio detection
- Shows extensive metadata in header (Meta Title, Meta Description, Primary/Secondary Keywords)
- Container max-width: 1440px
---
## 📐 DESIGN PLAN
### 1. CSS Container Width Update
```
.igny8-content-container {
max-width: 1280px; /* Default for screens <= 1600px */
}
@media (min-width: 1600px) {
.igny8-content-container {
max-width: 1530px; /* For screens > 1600px */
}
}
```
---
### 2. WordPress Header Redesign
**USER-FACING FIELDS (Keep in Header):**
| Field | Display | Icon | Notes |
|-------|---------|------|-------|
| Title | H1 | - | Post title |
| Status Badge | Published/Draft/etc | - | Post status |
| Posted Date | Formatted date | Calendar SVG | Publication date |
| Word Count | Formatted number | Document SVG | Content word count |
| Author | Author name | User SVG | Post author |
| Topic | Cluster name (clickable)| Compass SVG | Display cluster_name as "Topic" |
| Categories | Badge list (Parent > Child clicakble) | Folder SVG | WP Categories |
| Tags | Badge list (clickable)| Tag SVG | WP Tags |
**NON-USER FIELDS (Move to Metadata Section - Editor+ only):**
- Content ID, Task ID
- Content Type, Structure
- Cluster ID (keep cluster_name as Topic in header)
- Sector ID, Sector Name
- Primary Keyword, Secondary Keywords
- Meta Title, Meta Description
- Source, Last Synced
---
### 3. Section Label Redesign
**Current:** "Section Spotlight" (generic text)
**New Approach - Keyword/Tag Matching Algorithm:**
1. **Source Data:**
- Get all WordPress tags assigned to the post
- Get all WordPress categories assigned to the post
- Get primary keyword from post meta
- Get secondary keywords from post meta (if available)
2. **Matching Logic:**
- For each section heading (H2), perform case-insensitive partial matching
- Check if any tag name appears in the heading text
- Check if any category name appears in the heading text
- Check if primary/secondary keywords appear in the heading text
- Prioritize: Primary Keyword > Tags > Categories > Secondary Keywords
3. **Display Rules:**
- If matches found: Display up to 2 matched keywords/tags as badges
- If no matches: Display topic (cluster_name) or leave section without label badges
- Never display generic "Section Spotlight" text
4. **Badge Styling:**
```
[Primary Match] [Secondary Match] ← styled badges replacing "Section Spotlight"
```
**Colors:**
- Primary badge: `theme-color @ 15% opacity` background, `theme-color` text
- Secondary badge: `theme-color @ 8% opacity` background, `theme-color @ 80%` text
**Implementation Function (Pseudo-code):**
```php
function igny8_get_section_badges($heading, $post_id) {
$badges = [];
$heading_lower = strtolower($heading);
// Get post taxonomies and keywords
$tags = get_the_tags($post_id);
$categories = get_the_category($post_id);
$primary_kw = get_post_meta($post_id, '_igny8_primary_keyword', true);
$secondary_kws = get_post_meta($post_id, '_igny8_secondary_keywords', true);
// Priority 1: Primary keyword
if ($primary_kw && stripos($heading_lower, strtolower($primary_kw)) !== false) {
$badges[] = ['text' => $primary_kw, 'type' => 'primary'];
}
// Priority 2: Tags
if ($tags && count($badges) < 2) {
foreach ($tags as $tag) {
if (stripos($heading_lower, strtolower($tag->name)) !== false) {
$badges[] = ['text' => $tag->name, 'type' => 'tag'];
if (count($badges) >= 2) break;
}
}
}
// Priority 3: Categories
if ($categories && count($badges) < 2) {
foreach ($categories as $cat) {
if (stripos($heading_lower, strtolower($cat->name)) !== false) {
$badges[] = ['text' => $cat->name, 'type' => 'category'];
if (count($badges) >= 2) break;
}
}
}
// Priority 4: Secondary keywords
if ($secondary_kws && count($badges) < 2) {
$kw_array = is_array($secondary_kws) ? $secondary_kws : explode(',', $secondary_kws);
foreach ($kw_array as $kw) {
$kw = trim($kw);
if (stripos($heading_lower, strtolower($kw)) !== false) {
$badges[] = ['text' => $kw, 'type' => 'keyword'];
if (count($badges) >= 2) break;
}
}
}
return $badges;
}
```
---
### 4. Image Distribution Strategy
**Available Images (4 total):**
- Position 0: Square (1024×1024)
- Position 1: Landscape (1536×1024 or 1920×1080)
- Position 2: Square (1024×1024)
- Position 3: Landscape (1536×1024 or 1920×1080)
**Distribution Plan - First 4 Sections (with descriptions):**
| Section | Image Position | Type | Width | Alignment | Description |
|---------|---------------|------|-------|-----------|-------------|
| **Featured** | Position 1 | Landscape | 100% max 1024px | Center | Show prompt on first use |
| **Section 1** | Position 0 | Square | 50% | Right | With description + widget placeholder below |
| **Section 2** | Position 3 | Landscape | 100% max 1024px | Full width | With description |
| **Section 3** | Position 2 | Square | 50% | Left | With description + widget placeholder below |
| **Section 4** | Position 1 (reuse) | Landscape | 100% max 1024px | Full width | With description |
**Distribution Plan - Sections 5-7+ (reuse without descriptions):**
| Section | Reuse Image | Type | Width | Alignment | Description |
|---------|-------------|------|-------|-----------|-------------|
| **Section 5** | Featured (pos 1) | Landscape | 100% max 1024px | Full width | NO description |
| **Section 6** | Position 0 | Square | 50% | Right | NO description + widget placeholder |
| **Section 7** | Position 3 | Landscape | 100% max 1024px | Full width | NO description |
| **Section 8+** | Cycle through all 4 | Based on type | Based on type | Based on type | NO description |
**Special Case - Tables:**
- When section contains `<table>` element, always place full-width landscape image BEFORE table
- Use next available landscape image (Position 1 or 3)
- Max width: 1024px, centered
- Spacing: `margin-bottom: 2rem` before table
- Override normal section pattern when table detected
**Image Reuse Rules:**
- Images 1-4 used in first 4 sections WITH descriptions/prompts
- Sections 5+ reuse same images WITHOUT descriptions/prompts
- Use CSS classes: `.igny8-image-first-use` vs `.igny8-image-reuse`
- Maintain same layout pattern (square = 50%, landscape = 100%)
**Widget Placeholders:**
- Show only below square images (left/right aligned)
- Empty div with class `.igny8-widget-placeholder`
- Space reserved for future widget insertion
- Controlled via plugin settings (future implementation)
**Implementation Notes:**
```php
// Check for table in section content
function igny8_section_has_table($section_html) {
return (stripos($section_html, '<table') !== false);
}
// Get image aspect ratio from position
function igny8_get_image_aspect($position) {
// Even positions (0, 2) = square
// Odd positions (1, 3) = landscape
return ($position % 2 === 0) ? 'square' : 'landscape';
}
// Determine if image should show description
function igny8_show_image_description($section_index) {
// First 4 sections (0-3) show descriptions
return ($section_index < 4);
}
```
---
### 5. Image Alignment with Tables
When section contains a `<table>`:
- Place landscape image ABOVE the table
- Full width (max 800px)
- Proper spacing: `margin-bottom: 2rem`
- Table should not wrap around image
---
### 6. Responsive Image Width Rules
```css
/* Landscape images */
.igny8-image-landscape {
max-width: 1024px; /* Updated from 800px */
width: 100%;
margin: 0 auto;
display: block;
}
.igny8-image-landscape.igny8-image-reuse {
/* No description shown on reuse */
}
/* Single square image - Right aligned */
.igny8-image-square-right {
max-width: 50%;
margin-left: auto;
float: right;
margin-left: 2rem;
margin-bottom: 2rem;
}
/* Single square image - Left aligned */
.igny8-image-square-left {
max-width: 50%;
margin-right: auto;
float: left;
margin-right: 2rem;
margin-bottom: 2rem;
}
/* Widget placeholder below square images */
.igny8-widget-placeholder {
clear: both;
min-height: 200px;
padding: 1.5rem;
margin-top: 1rem;
background: rgba(0, 0, 0, 0.02);
border: 1px dashed rgba(0, 0, 0, 0.1);
border-radius: 12px;
display: none; /* Hidden by default, shown when widgets enabled */
}
.igny8-widget-placeholder.igny8-widgets-enabled {
display: block;
}
/* Table-specific image positioning */
.igny8-image-before-table {
max-width: 1024px;
width: 100%;
margin: 0 auto 2rem;
display: block;
}
```
---
### 7. Role-Based Visibility
**Metadata Section (Bottom):**
```php
<?php if (current_user_can('edit_posts')): ?>
<div class="igny8-metadata-footer igny8-editor-only">
<!-- All internal metadata here -->
</div>
<?php endif; ?>
```
**Visible only to:**
- Editor
- Administrator
- Author (for their own posts)
---
### 8. Header Icon Set (Replace Emojis)
Create inline SVG icons matching theme color:
```php
// Calendar Icon
<svg class="igny8-icon" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z"/>
</svg>
// Document Icon (Word Count)
<svg class="igny8-icon" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z"/>
</svg>
// User Icon (Author)
<svg class="igny8-icon" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"/>
</svg>
// Compass Icon (Topic/Cluster)
<svg class="igny8-icon" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z"/>
</svg>
// Folder Icon (Categories)
<svg class="igny8-icon" viewBox="0 0 20 20" fill="currentColor">
<path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"/>
</svg>
// Tag Icon (Tags)
<svg class="igny8-icon" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M17.707 9.293a1 1 0 010 1.414l-7 7a1 1 0 01-1.414 0l-7-7A.997.997 0 012 10V5a3 3 0 013-3h5c.256 0 .512.098.707.293l7 7zM5 6a1 1 0 100-2 1 1 0 000 2z"/>
</svg>
```
Icon styling:
```css
.igny8-icon {
width: 1rem;
height: 1rem;
color: var(--igny8-theme-color, currentColor);
opacity: 0.8;
display: inline-block;
vertical-align: middle;
}
```
---
### 9. Table of Contents
**Position:** Below featured image, before intro section
**Content:** List all H2 headings from content
**Features:**
- Clickable links with smooth scroll to sections
- Collapsible/expandable (optional)
- Numbered list matching section numbers
- Sticky positioning option (future setting)
**Implementation:**
```php
function igny8_generate_table_of_contents($content) {
$toc_items = [];
// Parse content for H2 headings
preg_match_all('/<h2[^>]*>(.*?)<\/h2>/i', $content, $matches);
if (!empty($matches[1])) {
foreach ($matches[1] as $index => $heading) {
$heading_text = strip_tags($heading);
$slug = sanitize_title($heading_text);
$toc_items[] = [
'number' => $index + 1,
'text' => $heading_text,
'id' => $slug
];
}
}
return $toc_items;
}
```
**HTML Structure:**
```html
<nav class="igny8-table-of-contents">
<div class="igny8-toc-header">
<span class="igny8-toc-icon">📑</span>
<h3>Table of Contents</h3>
</div>
<ol class="igny8-toc-list">
<?php foreach ($toc_items as $item): ?>
<li class="igny8-toc-item">
<a href="#<?php echo esc_attr($item['id']); ?>" class="igny8-toc-link">
<span class="igny8-toc-number"><?php echo $item['number']; ?>.</span>
<span class="igny8-toc-text"><?php echo esc_html($item['text']); ?></span>
</a>
</li>
<?php endforeach; ?>
</ol>
</nav>
```
**CSS:**
```css
.igny8-table-of-contents {
background: var(--wp--preset--color--base, #ffffff);
border: 2px solid rgba(0, 0, 0, 0.12);
border-radius: 16px;
padding: 1.5rem 2rem;
margin-bottom: 2rem;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.igny8-toc-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
}
.igny8-toc-header h3 {
margin: 0;
font-size: 1.125rem;
font-weight: 600;
}
.igny8-toc-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.igny8-toc-link {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 0.75rem;
text-decoration: none;
color: inherit;
border-radius: 8px;
transition: background-color 0.2s ease;
}
.igny8-toc-link:hover {
background: rgba(0, 0, 0, 0.04);
}
.igny8-toc-number {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 1.5rem;
background: rgba(59, 130, 246, 0.1);
color: rgba(59, 130, 246, 1);
border-radius: 50%;
font-size: 0.75rem;
font-weight: 600;
flex-shrink: 0;
}
.igny8-toc-text {
flex: 1;
font-size: 0.9375rem;
}
```
**Settings (Future Implementation):**
```php
// Plugin settings for TOC
$igny8_toc_settings = [
'enabled' => true,
'show_numbers' => true,
'collapsible' => false,
'sticky' => false,
'min_headings' => 3, // Only show if 3+ H2 headings
];
```
---
### 10. Widget System
**Widget Placeholders:**
Widgets appear below square images (left/right aligned) where there's natural space.
**Placeholder Function:**
```php
function igny8_render_widget_placeholder($position, $section_index) {
// Check if widgets are enabled in settings
$widgets_enabled = get_option('igny8_widgets_enabled', false);
if (!$widgets_enabled) {
return '';
}
$placeholder_class = 'igny8-widget-placeholder igny8-widgets-enabled';
$placeholder_class .= ' igny8-widget-' . $position; // left or right
$placeholder_class .= ' igny8-widget-section-' . $section_index;
?>
<div class="<?php echo esc_attr($placeholder_class); ?>"
data-widget-position="<?php echo esc_attr($position); ?>"
data-section-index="<?php echo esc_attr($section_index); ?>">
<!-- Widget content will be inserted here via settings -->
<?php do_action('igny8_widget_placeholder', $position, $section_index); ?>
</div>
<?php
}
```
**Widget Settings (Future Implementation):**
```php
// Plugin settings for widgets
$igny8_widget_settings = [
'enabled' => false,
'sections' => [
'section_1' => [
'position' => 'right',
'widget_type' => 'related_posts', // or 'custom_html', 'ad_unit', etc.
'content' => '',
],
'section_3' => [
'position' => 'left',
'widget_type' => 'newsletter_signup',
'content' => '',
],
],
];
```
**Widget Types (Future):**
- Related Posts
- Newsletter Signup
- Ad Units
- Custom HTML
- Social Share Buttons
- Author Bio
- Call-to-Action Boxes
---
### 11. Updated Structure Overview
**WordPress Single Post:**
```
┌─────────────────────────────────────────────┐
│ HEADER │
│ ← Back to Posts │
│ │
│ [H1 Title] [Status Badge] │
│ │
│ 📅 Posted: Date 📄 Words 👤 Author │
│ 🧭 Topic: Cluster Name │
│ 📁 [Category Badges] 🏷️ [Tag Badges] │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ FEATURED IMAGE (Landscape, max 1024px) │
│ │
│ [Image Prompt - first use only] │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ TABLE OF CONTENTS │
│ 📑 Table of Contents │
│ 1. Section Heading One │
│ 2. Section Heading Two │
│ 3. Section Heading Three │
│ ... (clickable, smooth scroll) │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ INTRO SECTION │
│ Opening Narrative │
│ [Content...] │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ SECTION 1 │
│ [Keyword Badge] [Tag Badge] │
│ 1 [H2 Heading] │
│ │
│ ┌──────────────┐ ┌──────────────────────┐ │
│ │ │ │ │ │
│ │ Content │ │ Square Image (50%) │ │
│ │ │ │ Right Aligned │ │
│ │ │ │ [Image Description] │ │
│ └──────────────┘ └──────────────────────┘ │
│ [Widget Placeholder] │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ SECTION 2 │
│ [Keyword Badge] │
│ 2 [H2 Heading] │
│ │
│ ┌───────────────────────────────────────┐ │
│ │ Landscape Image (100% max 1024px) │ │
│ │ [Image Description] │ │
│ └───────────────────────────────────────┘ │
│ │
│ [Content...] │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ SECTION 3 │
│ [Keyword Badge] [Tag Badge] │
│ 3 [H2 Heading] │
│ │
│ ┌──────────────────────┐ ┌──────────────┐ │
│ │ │ │ │ │
│ │ Square Image (50%) │ │ Content │ │
│ │ Left Aligned │ │ │ │
│ │ [Image Description] │ │ │ │
│ └──────────────────────┘ └──────────────┘ │
│ [Widget Placeholder] │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ SECTION 4 (with table example) │
│ [Keyword Badge] │
│ 4 [H2 Heading] │
│ │
│ ┌───────────────────────────────────────┐ │
│ │ Landscape Image (100% max 1024px) │ │
│ │ [Image Description] │ │
│ └───────────────────────────────────────┘ │
│ │
│ [Content before table...] │
│ │
│ ┌───────────────────────────────────────┐ │
│ │ TABLE │ │
│ │ [Data rows and columns] │ │
│ └───────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ SECTION 5 (reuse - no description) │
│ [Keyword Badge] │
│ 5 [H2 Heading] │
│ │
│ ┌───────────────────────────────────────┐ │
│ │ Featured Image REUSED (no caption) │ │
│ └───────────────────────────────────────┘ │
│ │
│ [Content...] │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ SECTION 6 (reuse - no description) │
│ [Tag Badge] │
│ 6 [H2 Heading] │
│ │
│ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Content │ │ Square Image REUSED │ │
│ │ │ │ (no caption) │ │
│ └──────────────┘ └──────────────────────┘ │
│ [Widget Placeholder] │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ METADATA FOOTER (Editor+ only) │
│ ▸ View IGNY8 Metadata │
│ - Content ID: 123 │
│ - Task ID: 456 │
│ - Meta Title: ... │
│ - Meta Description: ... │
│ - Primary Keyword: ... │
│ - Secondary Keywords: [list] │
│ - Cluster ID: 789 │
│ - Sector: Industry Name │
│ - Source: AI Generated │
│ - Last Synced: Date/Time │
└─────────────────────────────────────────────┘
```
---
### 12. App ContentViewTemplate Updates
**Changes to body section only (not header):**
1. **Remove "Section Spotlight" label** - Replace with keyword badge matching system
2. **Add Table of Contents** below featured image (matching WordPress implementation)
3. **Match image layout rules** from WordPress template:
- Section 1: Square right-aligned 50% (with description)
- Section 2: Landscape full width max 1024px (with description)
- Section 3: Square left-aligned 50% (with description)
- Section 4: Landscape full width max 1024px (with description)
- Sections 5+: Reuse images without descriptions
4. **Featured image** max 1024px centered
5. **Widget placeholders** below square images (empty for now)
6. **Table detection** - full-width image before tables
**Implementation Priority:**
- Phase 1: Update image sizing (1024px max)
- Phase 2: Implement keyword badge matching
- Phase 3: Add table of contents component
- Phase 4: Add widget placeholder divs
---
## Summary of Files to Update
| File | Changes | Priority |
|------|---------|----------|
| `igny8-content-template.css` | Container width breakpoints, image sizing classes, TOC styles, widget placeholder styles | 🔴 High |
| `igny8-header.php` | Remove emojis, add SVG icons, add Topic field, remove SEO/internal metadata | 🔴 High |
| `igny8-metadata.php` | Add role check (`current_user_can('edit_posts')`), include all moved metadata fields | 🔴 High |
| `igny8-content-sections.php` | Keyword badge matching logic, smart image distribution (Section 1-4 pattern), widget placeholders | 🔴 High |
| `igny8-featured-image.php` | Max 1024px, landscape priority | 🟡 Medium |
| `includes/template-functions.php` | Add helper functions: `igny8_get_section_badges()`, `igny8_section_has_table()`, `igny8_show_image_description()`, `igny8_generate_table_of_contents()` | 🔴 High |
| `ContentViewTemplate.tsx` | Match section labels, image layouts, add TOC component, widget placeholders | 🟡 Medium |
| **New File**: `parts/igny8-table-of-contents.php` | Table of contents component | 🟡 Medium |
| **New File**: `admin/settings-page.php` | Widget settings, TOC settings (future) | 🟢 Low |
---
## Configuration Settings (Future Implementation)
```php
// Plugin settings structure
$igny8_plugin_settings = [
'table_of_contents' => [
'enabled' => true,
'show_numbers' => true,
'collapsible' => false,
'sticky' => false,
'min_headings' => 3,
'position' => 'after_featured_image', // or 'before_content', 'floating'
],
'widgets' => [
'enabled' => false,
'sections' => [
'section_1' => [
'position' => 'right',
'widget_type' => 'none', // 'related_posts', 'custom_html', 'ad_unit', etc.
'content' => '',
],
'section_3' => [
'position' => 'left',
'widget_type' => 'none',
'content' => '',
],
],
],
'images' => [
'featured_max_width' => 1024,
'landscape_max_width' => 1024,
'square_width_percentage' => 50,
'show_descriptions_sections' => 4, // Show descriptions in first N sections
],
'badges' => [
'show_section_badges' => true,
'max_badges_per_section' => 2,
'badge_sources' => ['primary_keyword', 'tags', 'categories', 'secondary_keywords'], // Priority order
],
];
```
---
## Implementation Phases
### Phase 1: Core Template Updates (Week 1)
- ✅ Update CSS container widths and image sizing
- ✅ Replace emojis with SVG icons in header
- ✅ Add Topic field to header
- ✅ Move metadata to bottom with role check
- ✅ Implement keyword badge matching logic
### Phase 2: Advanced Features (Week 2)
- ✅ Table of contents component
- ✅ Table detection and image positioning
- ✅ Image reuse logic (sections 5+)
### Phase 3: App Sync (Week 3)
- ✅ Update ContentViewTemplate.tsx to match WordPress
- ✅ Add TOC component to React app
- ✅ Sync image layouts and sizing
### Phase 4: Settings & Configuration (Week 4)
- ⏳ Plugin settings page
- ⏳ TOC configuration options
- ⏳ Widget management interface
- ⏳ Badge display preferences
---
**Last Updated:** January 10, 2026
**Document Version:** 2.0
**Status:** Design Complete - Ready for Implementation