Files
igny8/docs/igny8-app/05-WRITER-IMAGES-PAGE-SYSTEM-DESIGN.md
IGNY8 VPS (Salman) 30bbcb08a1 asdadd
2025-12-03 14:20:14 +00:00

33 KiB

Writer Images Page - Complete System Design & Implementation Document

Version: 1.0
Last Updated: 2025-11-28
Status: Fully Implemented with WordPress Publishing Integration
Scope: End-to-End Analysis of /writer/images Page with Manual & Automated Publishing


Table of Contents

  1. Overview
  2. Architecture
  3. Component Structure
  4. State Management
  5. API Functions Chain
  6. Data Flow & Lifecycle
  7. WordPress Publishing System
  8. Automated Publishing
  9. Sync Functions
  10. Error Handling
  11. Performance Optimizations

Overview

The /writer/images page is a comprehensive content image management interface that enables users to:

  • View grouped content with featured and in-article images
  • Generate AI images with real-time progress tracking
  • Publish content to WordPress (manual or bulk)
  • Update image statuses
  • Monitor publication sync status

Key Features

  • Client-side pagination, filtering, and sorting
  • Real-time image generation queue with modal
  • Individual and bulk WordPress publishing
  • Unified API response handling
  • Automatic status synchronization with WordPress
  • Comprehensive error handling with retry logic
  • Resource debug logging (AI Function Logs)

Architecture

High-Level System Flow

User Interaction (Images Page)
    ↓
Frontend Page Component (Images.tsx)
    ↓
Configuration Layer (images.config.tsx, table-actions.config.tsx)
    ↓
API Service Layer (api.ts with fetchAPI)
    ↓
Unified Response Handler (success/error extraction)
    ↓
Backend Endpoints (/v1/writer/images/*, /v1/publisher/publish/)
    ↓
WordPress Bridge Integration
    ↓
WordPress Site Publication

Technology Stack

Layer Technology Purpose
Frontend React 18 + TypeScript UI Component Framework
State Management React Hooks (useState, useCallback, useEffect) Local component state
API Client fetchAPI() from services/api.ts Unified API communication
UI Components TablePageTemplate, Modal Components Reusable UI elements
Backend API Django REST Framework REST endpoints (/v1/writer/images/, /v1/publisher/publish/)
Publishing WordPress REST API via Bridge Content publication target

Component Structure

Main Component: Images.tsx

File: frontend/src/pages/Writer/Images.tsx
Lines: 738 total
Purpose: Main page component managing all image-related operations

Component Hierarchy

Images (Page Component)
├── PageHeader (with navigation tabs)
├── TablePageTemplate (main data table)
│   ├── Filter UI
│   ├── Table Rows (ContentImagesGroup)
│   ├── Pagination Controls
│   └── Action Buttons (Row & Bulk Actions)
├── ImageQueueModal (image generation progress)
├── SingleRecordStatusUpdateModal (status updates)
└── Modal (image preview)

Configuration Components

1. images.config.tsx

  • Defines page columns (featured image, in-article images 1-5+)
  • Filter configurations (search, status)
  • Header metrics (calculations)
  • Column rendering functions

2. table-actions.config.tsx

  • Path: /writer/images
  • Row Actions:
    • publish_wordpress - Publish single content to WordPress
    • update_status - Change image status
  • Bulk Actions:
    • bulk_publish_wordpress - Publish multiple items
  • Visibility Logic (shouldShow):
    shouldShow: (row: any) => {
      return row.status === 'published' && 
             (!row.external_id || !row.external_url) &&
             (!row.sync_status || row.sync_status !== 'published');
    }
    

State Management

Page-Level State Variables

// Data State
const [images, setImages] = useState<ContentImagesGroup[]>([]);
const [loading, setLoading] = useState(true);

// Filter State
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('');
const [selectedIds, setSelectedIds] = useState<string[]>([]);

// Pagination State (Client-side)
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [totalCount, setTotalCount] = useState(0);
const pageSize = 10; // Items per page

// Sorting State
const [sortBy, setSortBy] = useState<string>('content_title');
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');

// Image Generation Modal
const [isQueueModalOpen, setIsQueueModalOpen] = useState(false);
const [imageQueue, setImageQueue] = useState<ImageQueueItem[]>([]);
const [currentContentId, setCurrentContentId] = useState<number | null>(null);
const [taskId, setTaskId] = useState<string | null>(null);
const [imageModel, setImageModel] = useState<string | null>(null);
const [imageProvider, setImageProvider] = useState<string | null>(null);

// Status Update Modal
const [isStatusModalOpen, setIsStatusModalOpen] = useState(false);
const [statusUpdateContentId, setStatusUpdateContentId] = useState<number | null>(null);
const [statusUpdateRecordName, setStatusUpdateRecordName] = useState<string>('');
const [isUpdatingStatus, setIsUpdatingStatus] = useState(false);

// Image Preview Modal
const [isImageModalOpen, setIsImageModalOpen] = useState(false);
const [modalImageUrl, setModalImageUrl] = useState<string | null>(null);

// Debug State
const [aiLogs, setAiLogs] = useState<Array<{...}>>([]); // AI Function Logs

API Functions Chain

Complete API Imports

import {
  fetchContentImages,              // GET /v1/writer/images/content_images/
  ContentImagesGroup,              // Type definition
  ContentImagesResponse,           // Type definition
  fetchImageGenerationSettings,    // GET /v1/system/integrations/image_generation/
  generateImages,                  // POST /v1/writer/images/generate_images/
  bulkUpdateImagesStatus,          // POST /v1/writer/images/bulk_update/
  ContentImage,                    // Type definition
  fetchAPI,                        // Unified API fetch wrapper
} from '../../services/api';

API Function Definitions (From api.ts)

1. fetchContentImages()

export async function fetchContentImages(
  filters: ContentImagesFilters = {}
): Promise<ContentImagesResponse> {
  const params = new URLSearchParams();
  
  // Auto-inject site filter
  if (!filters.site_id) {
    const activeSiteId = getActiveSiteId();
    if (activeSiteId) {
      filters.site_id = activeSiteId;
    }
  }
  
  // Auto-inject sector filter
  if (filters.sector_id === undefined) {
    const activeSectorId = getActiveSectorId();
    if (activeSectorId !== null && activeSectorId !== undefined) {
      filters.sector_id = activeSectorId;
    }
  }
  
  if (filters.site_id) params.append('site_id', filters.site_id.toString());
  if (filters.sector_id) params.append('sector_id', filters.sector_id.toString());
  
  const queryString = params.toString();
  return fetchAPI(`/v1/writer/images/content_images/${queryString ? `?${queryString}` : ''}`);
}

Purpose: Fetch content images grouped by content
Endpoint: GET /v1/writer/images/content_images/
Response Type: ContentImagesResponse

Response Structure:

{
  count: number;
  results: ContentImagesGroup[];
}

// ContentImagesGroup structure:
{
  content_id: number;
  content_title: string;
  featured_image: ContentImage | null;      // Featured image
  in_article_images: ContentImage[];        // 1-5+ in-article images
  overall_status: 'pending' | 'partial' | 'complete' | 'failed';
  status?: string;                          // 'published' or 'draft'
  external_id?: string;                     // WordPress post ID
  external_url?: string;                    // WordPress post URL
  sync_status?: string;                     // 'published', 'synced', etc.
}

// ContentImage structure:
{
  id: number;
  image_type: string;                       // 'featured' or 'in_article'
  image_url?: string | null;                // Generated image URL
  image_path?: string | null;               // Local file path
  prompt?: string | null;                   // Generation prompt
  status: string;                           // 'pending', 'generated', 'failed'
  position: number;                         // Order for in-article images
  created_at: string;
  updated_at: string;
}

2. generateImages()

export async function generateImages(
  imageIds: number[],
  contentId?: number
): Promise<{ success: boolean; task_id?: string; message?: string; error?: string }> {
  try {
    const response = await fetchAPI('/v1/writer/images/generate_images/', {
      method: 'POST',
      body: JSON.stringify({ 
        ids: imageIds,
        content_id: contentId 
      }),
    });
    
    return { success: true, ...response } as any;
  } catch (error: any) {
    if (error.response && typeof error.response === 'object') {
      return { success: false, error: error.message, ...error.response } as any;
    }
    throw error;
  }
}

Purpose: Start image generation for specified image IDs
Endpoint: POST /v1/writer/images/generate_images/
Request Body:

{
  "ids": [123, 124, 125],
  "content_id": 42
}

Response:

{
  "success": true,
  "task_id": "celery-task-uuid",
  "message": "Image generation started",
  "images_created": 3
}

3. bulkUpdateImagesStatus()

export async function bulkUpdateImagesStatus(
  contentId: number,
  status: string
): Promise<{ updated_count: number }> {
  return fetchAPI(`/v1/writer/images/bulk_update/`, {
    method: 'POST',
    body: JSON.stringify({ content_id: contentId, status }),
  });
}

Purpose: Update status for all images of a content
Endpoint: POST /v1/writer/images/bulk_update/
Request Body:

{
  "content_id": 42,
  "status": "generated"
}

Response:

{
  "updated_count": 4
}

4. fetchImageGenerationSettings()

export async function fetchImageGenerationSettings(): Promise<ImageGenerationSettings> {
  return fetchAPI('/v1/system/integrations/image_generation/');
}

Purpose: Get image generation configuration
Endpoint: GET /v1/system/integrations/image_generation/
Response:

{
  success: boolean;
  config: {
    provider: string;              // 'openai', 'stability', etc.
    model: string;                 // 'dall-e-3', 'stable-diffusion', etc.
    image_type: string;            // 'featured' or 'in_article'
    max_in_article_images: number; // Usually 2-5
    image_format: string;          // 'jpg', 'png', etc.
    desktop_enabled: boolean;
    mobile_enabled: boolean;
  }
}

5. fetchAPI() - Unified API Wrapper

export async function fetchAPI(
  endpoint: string,
  options?: RequestInit & { timeout?: number }
): Promise<any> {
  // 1. Auto-injects JWT token from auth store
  // 2. Handles 401 with token refresh
  // 3. Extracts unified response format
  // 4. Transforms error responses
  // 5. Handles network errors
}

Response Handling:

  • Unified Success: { success: true, data: {...} } → returns data
  • Paginated: { success: true, count: X, results: [...] } → returns as-is
  • Unified Error: { success: false, error: "..." } → throws error
  • Non-200 Status: Throws with error details

Data Flow & Lifecycle

1. Page Load Flow

Page Mount
  → loadImages() called in useEffect
    → fetchContentImages({}) 
      → API: GET /v1/writer/images/content_images/
        ↓
        Response: {count: X, results: [ContentImagesGroup]}
      ← Parse & filter (client-side)
        • Search filter
        • Status filter
        • Sort by title/status
        • Paginate (10 per page)
      → setImages(paginatedResults)
      → setShowContent(true)
      → UI renders table with data

2. Image Generation Flow

User clicks "Generate Images" button for content
  → handleGenerateImages(contentId) triggered
    → Fetch max_in_article_images from settings
    → Build image queue from pending images
    → Open ImageQueueModal with queue
    
User clicks "Start Generation"
  → generateImages(imageIds, contentId) called
    → API: POST /v1/writer/images/generate_images/
      ↓
      Returns task_id (Celery task)
    ← Modal starts polling for progress
      • Updates progress bars per image
      • Shows real-time generation status
      
Generation Complete
  → Modal closes
  → loadImages() called to refresh
  → Table shows generated images

3. Manual WordPress Publishing Flow

User clicks "Publish to WordPress" on a row
  → handleRowAction('publish_wordpress', row) triggered
    → Check conditions:
        • row.status === 'published'
        • No external_id OR No external_url
        • sync_status !== 'published'
    
    → fetchAPI('/v1/publisher/publish/', {
        method: 'POST',
        body: {
          content_id: row.content_id,
          destinations: ['wordpress']
        }
      })
    
    → Backend: /api/v1/publisher/publish/
      ↓
      Receives: {content_id: 42, destinations: ['wordpress']}
      
      Calls: PublisherService.publish_content()
        → Finds Content object by ID
        → Maps content to WordPress post format
        → Creates/updates post via WordPress Bridge
        → Sets external_id, external_url, sync_status
      
      Returns: {
        success: true,
        data: {
          content_id: 42,
          external_id: "5678",
          external_url: "https://site.com/post-title",
          sync_status: "published"
        }
      }
    
    ← Frontend receives response
    → toast.success('Published to WordPress')
    → loadImages() to refresh status
    → Table updates to show published status

4. Bulk WordPress Publishing Flow

User selects multiple items
  → Click "Publish Ready to WordPress"
  → handleBulkAction('bulk_publish_wordpress', ids) triggered
    
    → Filter items that are ready:
      const readyItems = images
        .filter(item => ids.includes(item.content_id))
        .filter(item => 
          item.status === 'published' &&
          (!item.external_id || !item.external_url) &&
          (!item.sync_status || item.sync_status !== 'published')
        )
    
    → For each readyItem:
        fetchAPI('/v1/publisher/publish/', {
          method: 'POST',
          body: {
            content_id: item.content_id,
            destinations: ['wordpress']
          }
        })
      
      Track: successCount++, failedCount++
    
    → Show summary toast:
      "Published X items, Y failed"
    
    → loadImages() to refresh all

5. Status Update Flow

User clicks "Update Status"
  → Modal opens for status selection
  → User selects: 'pending' | 'generated' | 'failed'
  → handleStatusUpdate(status) called
    
    → bulkUpdateImagesStatus(contentId, status)
      → API: POST /v1/writer/images/bulk_update/
        {content_id: 42, status: 'generated'}
      
      Returns: {updated_count: 4}
    
    ← Modal closes
    → toast.success('Updated 4 images')
    → loadImages() to refresh

WordPress Publishing System

Manual Publishing

Architecture

Frontend (Images Page)
  └── handleRowAction('publish_wordpress', row)
      └── fetchAPI('/v1/publisher/publish/', {...})
          └── Backend Publisher Module
              └── PublisherService.publish_content()
                  └── Content → WordPress Bridge
                      └── WordPress Site (via REST API)
                          └── WP Post Created/Updated

Conditions for Visibility

The "Publish to WordPress" button appears when ALL conditions are met:

  1. Content Status: row.status === 'published'

    • Content must be in published status internally
  2. No WordPress Record: !row.external_id || !row.external_url

    • Either no WordPress ID or no WordPress URL
    • Allows re-publishing if one is missing
  3. Not Already Synced: !row.sync_status || row.sync_status !== 'published'

    • sync_status is not 'published'
    • Allows re-sync if status changed

Request Structure

const response = await fetchAPI('/v1/publisher/publish/', {
  method: 'POST',
  body: JSON.stringify({
    content_id: 42,                    // Content to publish
    destinations: ['wordpress']        // Target platform
  })
});

Response Structure

{
  success: true,
  data: {
    content_id: 42,
    external_id: "5678",               // WordPress post ID
    external_url: "https://site.com/p/5678/",
    sync_status: "published",          // Status after publishing
    message: "Content published to WordPress"
  },
  request_id: "uuid"
}

Bulk Publishing

Flow

// Filter ready items
const readyItems = images
  .filter(item => ids.includes(item.content_id))
  .filter(item => item.status === 'published' && 
                  (!item.external_id || !item.external_url) &&
                  (!item.sync_status || item.sync_status !== 'published'))

// Publish each individually
for (const item of readyItems) {
  try {
    const response = await fetchAPI('/v1/publisher/publish/', {
      method: 'POST',
      body: JSON.stringify({
        content_id: item.content_id,
        destinations: ['wordpress']
      })
    })
    
    if (response.success) {
      successCount++
    } else {
      failedCount++
    }
  } catch (error) {
    failedCount++
  }
}

// Show results
if (successCount > 0) {
  toast.success(`Published ${successCount} items`)
}
if (failedCount > 0) {
  toast.warning(`${failedCount} failed`)
}

// Refresh to show updated status
loadImages()

Advantages of Individual Requests

  1. Error Isolation: Failure in one publish doesn't affect others
  2. Granular Tracking: Knows exactly which items succeeded/failed
  3. User Feedback: Can show detailed success/fail breakdown
  4. Partial Success: Users see partial results instead of total failure

Automated Publishing

Automatic Status Sync (Two-Way Sync)

The system includes automatic synchronization of content status between IGNY8 and WordPress:

Components

  1. WordPress Bridge Plugin (igny8-wp-integration)

    • Monitors WordPress post changes
    • Syncs status back to IGNY8 API
    • Location: /includes/sync/hooks.php
  2. IGNY8 Backend Sync Tasks (Celery)

    • Periodic status checks
    • Webhook receivers
    • Status reconciliation

Sync Direction

IGNY8 → WordPress:
  Content published in Writer
  → Publisher API called
  → WordPress Bridge receives payload
  → Post created/updated in WordPress

WordPress → IGNY8:
  Post updated in WordPress (draft/publish/trash)
  → WordPress Hook triggered
  → Bridge syncs back via PUT /writer/tasks/{id}/
  → IGNY8 Content status updated

Sync Status Values

Status Meaning Publishable?
draft Not yet published to WordPress Yes
pending Awaiting review in WordPress No
published Live on WordPress No (already published)
synced Initial sync complete No
failed Publication failed Yes (retry)

Sync Functions

1. loadImages() - Main Refresh Function

const loadImages = useCallback(async () => {
  setLoading(true);
  setShowContent(false);
  try {
    // Fetch all content images
    const data: ContentImagesResponse = await fetchContentImages({});
    let filteredResults = data.results || [];
    
    // Client-side search filter
    if (searchTerm) {
      filteredResults = filteredResults.filter(group => 
        group.content_title?.toLowerCase().includes(searchTerm.toLowerCase())
      );
    }
    
    // Client-side status filter
    if (statusFilter) {
      filteredResults = filteredResults.filter(group => 
        group.overall_status === statusFilter
      );
    }
    
    // Client-side sorting
    filteredResults.sort((a, b) => {
      let aVal: any = a.content_title;
      let bVal: any = b.content_title;
      
      if (sortBy === 'overall_status') {
        aVal = a.overall_status;
        bVal = b.overall_status;
      }
      
      if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1;
      if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1;
      return 0;
    });
    
    // Client-side pagination
    const startIndex = (currentPage - 1) * pageSize;
    const endIndex = startIndex + pageSize;
    const paginatedResults = filteredResults.slice(startIndex, endIndex);
    
    setImages(paginatedResults);
    setTotalCount(filteredResults.length);
    setTotalPages(Math.ceil(filteredResults.length / pageSize));
    
    setTimeout(() => {
      setShowContent(true);
      setLoading(false);
    }, 100);
  } catch (error: any) {
    console.error('Error loading images:', error);
    toast.error(`Failed to load images: ${error.message}`);
    setShowContent(true);
    setLoading(false);
  }
}, [currentPage, statusFilter, sortBy, sortDirection, searchTerm, toast]);

Called After:

  • Page mount
  • Search term changes
  • Status filter changes
  • Sort order changes
  • Page navigation
  • Publishing operations complete
  • Status updates complete

2. handleStatusUpdate() - Status Change Handler

const handleStatusUpdate = useCallback(async (status: string) => {
  if (!statusUpdateContentId) return;

  setIsUpdatingStatus(true);
  try {
    const result = await bulkUpdateImagesStatus(statusUpdateContentId, status);
    toast.success(`Successfully updated ${result.updated_count} image(s) status to ${status}`);
    setIsStatusModalOpen(false);
    setStatusUpdateContentId(null);
    setStatusUpdateRecordName('');
    // Reload images to reflect the changes
    loadImages();
  } catch (error: any) {
    toast.error(`Failed to update status: ${error.message}`);
  } finally {
    setIsUpdatingStatus(false);
  }
}, [statusUpdateContentId, toast, loadImages]);

Flow:

  1. User opens status modal
  2. Selects new status
  3. API call to update all images for content
  4. Refresh table to show changes

3. Site/Sector Change Listeners

useEffect(() => {
  const handleSiteChange = () => {
    loadImages();
  };
  
  const handleSectorChange = () => {
    loadImages();
  };
  
  window.addEventListener('siteChanged', handleSiteChange);
  window.addEventListener('sectorChanged', handleSectorChange);
  
  return () => {
    window.removeEventListener('siteChanged', handleSiteChange);
    window.removeEventListener('sectorChanged', handleSectorChange);
  };
}, [loadImages]);

Purpose: Auto-refresh when user switches site or sector

useEffect(() => {
  const timer = setTimeout(() => {
    if (currentPage === 1) {
      loadImages();
    } else {
      setCurrentPage(1);  // Reset to page 1
    }
  }, 500);  // Wait 500ms after user stops typing

  return () => clearTimeout(timer);
}, [searchTerm, currentPage, loadImages]);

Purpose: Avoid excessive API calls during search input


Error Handling

Unified Error Response Format

All API errors follow the unified format:

{
  success: false,
  error: "Human-readable error message",
  errors: {
    field_name: ["Field-specific error"]
  },
  request_id: "uuid"
}

Error Types Handled

Error Type Handling User Feedback
Network Error Caught by fetchAPI "Network error: Unable to reach API"
401 Unauthorized Token refresh attempted Auto-retry or "Session expired"
403 Forbidden Authentication check failed "Permission denied"
404 Not Found Resource doesn't exist "Content not found"
422 Validation Invalid request data "Invalid input: {field details}"
500 Server Error Backend exception "Server error occurred"
Timeout Request > 30 seconds "Request timeout"

Error Handling in Publishing

try {
  const response = await fetchAPI('/v1/publisher/publish/', {
    method: 'POST',
    body: JSON.stringify({
      content_id: item.content_id,
      destinations: ['wordpress']
    })
  });
  
  if (response.success) {
    successCount++;
  } else {
    // Unified error format
    console.warn(`Failed: ${response.error}`);
    failedCount++;
  }
} catch (error: any) {
  // Network or unknown error
  console.error('Error publishing:', error);
  failedCount++;
}

Performance Optimizations

1. Client-Side Processing

  • Search: Filtered in memory, not via API
  • Sorting: Local array sort, no API call
  • Pagination: 10 items per page, loaded once
  • Status Filter: In-memory filtering

Benefit: Faster UX, no additional API calls

2. Memoization

// Prevent unnecessary re-renders
const pageConfig = useMemo(() => {
  return createImagesPageConfig({...});
}, [searchTerm, statusFilter, maxInArticleImages, ...]);

const headerMetrics = useMemo(() => {
  return pageConfig.headerMetrics.map(...);
}, [pageConfig?.headerMetrics, images, totalCount]);

3. Debouncing

// 500ms delay on search to avoid API thrashing
useEffect(() => {
  const timer = setTimeout(() => {
    loadImages();
  }, 500);
  
  return () => clearTimeout(timer);
}, [searchTerm, ...]);

4. Lazy Loading (Potential)

// Could implement virtual scrolling for 1000+ items
// Currently: Fixed page size of 10 items

5. Caching (Backend)

The API automatically caches:

  • Content images (5-minute TTL)
  • Image generation settings (server-side)
  • Site metadata

Configuration Deep Dive

Table Actions Configuration

File: table-actions.config.tsx

'/writer/images': {
  rowActions: [
    {
      key: 'publish_wordpress',
      label: 'Publish to WordPress',
      icon: <ArrowRightIcon className="w-5 h-5" />,
      variant: 'success',
      shouldShow: (row: any) => {
        // Only show if ready for publishing
        return row.status === 'published' && 
               (!row.external_id || !row.external_url) &&
               (!row.sync_status || row.sync_status !== 'published');
      },
    },
    {
      key: 'update_status',
      label: 'Update Status',
      icon: <CheckCircleIcon className="w-5 h-5" />,
      variant: 'primary',
    },
  ],
  bulkActions: [
    {
      key: 'bulk_publish_wordpress',
      label: 'Publish Ready to WordPress',
      icon: <ArrowRightIcon className="w-5 h-5" />,
      variant: 'success',
    },
  ],
}

Images Page Config

File: images.config.tsx

export const createImagesPageConfig = (handlers: {
  searchTerm: string;
  setSearchTerm: (value: string) => void;
  statusFilter: string;
  setStatusFilter: (value: string) => void;
  setCurrentPage: (page: number) => void;
  maxInArticleImages?: number;
  onGenerateImages?: (contentId: number) => void;
  onImageClick?: (contentId: number, imageType: 'featured' | 'in_article', position?: number) => void;
}): ImagesPageConfig => {
  // Builds columns with featured image + up to 5 in-article images
  // Builds filters: search, status
  // Calculates header metrics
}

Data Models

ContentImagesGroup

interface ContentImagesGroup {
  content_id: number;
  content_title: string;
  featured_image: ContentImage | null;
  in_article_images: ContentImage[];
  overall_status: 'pending' | 'partial' | 'complete' | 'failed';
  
  // WordPress fields
  status?: string;              // 'published' or 'draft'
  external_id?: string;         // WordPress post ID
  external_url?: string;        // WordPress post URL
  sync_status?: string;         // 'published', 'synced', 'failed'
}

ContentImage

interface ContentImage {
  id: number;
  image_type: string;           // 'featured' | 'in_article'
  image_url?: string | null;    // Generated image URL
  image_path?: string | null;   // Local file path
  prompt?: string | null;       // Generation prompt
  status: string;               // 'pending' | 'generated' | 'failed'
  position: number;             // Order for in-article (1-5+)
  created_at: string;
  updated_at: string;
}

API Endpoints Reference

Reader Endpoints

Method Endpoint Purpose
GET /v1/writer/images/content_images/ Fetch grouped content images
GET /v1/system/integrations/image_generation/ Get generation settings

Writer Endpoints

Method Endpoint Purpose
POST /v1/writer/images/generate_images/ Start image generation
POST /v1/writer/images/bulk_update/ Update image statuses

Publisher Endpoints

Method Endpoint Purpose
POST /v1/publisher/publish/ Publish to destinations (WordPress)

Key Implementation Decisions

1. Unified Publishing Endpoint

Decision: Use existing /v1/publisher/publish/ instead of custom WordPress endpoints
Rationale:

  • Follows IGNY8 unified API standard
  • Supports multiple destinations (WordPress, Sites Renderer, etc.)
  • Centralized publishing logic
  • Easier to extend

2. Client-Side Processing

Decision: Perform search, filter, sort, paginate on client
Rationale:

  • Faster response (no API round-trips)
  • Better UX for small datasets (< 1000 items)
  • Works offline
  • Reduces server load

3. Individual Bulk Publishes

Decision: Loop and call API individually for bulk operations
Rationale:

  • Error isolation (one failure doesn't affect others)
  • Granular progress tracking
  • User sees partial success
  • Easier error reporting

4. Automatic Status Sync

Decision: Refresh data after publish operations
Rationale:

  • Ensures UI reflects latest server state
  • Shows WordPress sync status
  • Catches errors from background tasks
  • User sees immediate feedback

Future Enhancements

Potential Improvements

  1. Server-Side Pagination

    • Replace client-side pagination
    • Reduce memory usage for large datasets
    • Enable server-side filtering/sorting
  2. Real-Time Updates

    • WebSocket for publishing progress
    • Push notifications
    • Live sync status updates
  3. Batch Publishing API

    • Single endpoint for bulk operations
    • Reduced network overhead
    • Atomic operations (all-or-nothing)
  4. Advanced Filtering

    • Filter by image type (featured/in-article)
    • Filter by image status
    • Filter by sync status
  5. Scheduling

    • Schedule publishing for specific dates
    • Queue management UI
    • Publishing calendar
  6. Retry Mechanism

    • Automatic retry on failure
    • Exponential backoff
    • Dead letter queue for failed items

Testing Checklist

  • Load page with empty data
  • Load page with 50+ items
  • Search functionality
  • Filter by status
  • Sort by title/status
  • Pagination navigation
  • Generate images flow
  • Single item publish to WordPress
  • Bulk publish operation
  • Partial failure handling
  • Status update operation
  • Image preview modal
  • Error toasts display correctly
  • Reload on site change
  • Reload on sector change
  • Token refresh on 401
  • Network error handling

Debugging & Support

Enable Resource Debug Logs

// In any page using Images component
const resourceDebugEnabled = useResourceDebug();
// Shows AI Function Logs panel with all operations

Check API Responses

// In browser DevTools
// Network tab → XHR/Fetch → Filter "images"
// View each request/response

// Look for unified format:
{
  "success": true,
  "data": {...},
  "request_id": "uuid"
}

Common Issues

Issue Cause Solution
Publish button not showing Status != 'published' OR already has external_id Check row.status and row.external_id
Publishing succeeds but data not updated Need to refresh loadImages() called, wait for refresh
Images not loading Site/sector not selected Select site in header
API 401 error Token expired Should auto-refresh, check auth store

Conclusion

The /writer/images page implements a comprehensive content image management system with:

Full Publishing Workflow - Manual and bulk publishing to WordPress
Real-Time Generation - Image generation with progress tracking
Two-Way Sync - Automatic status synchronization with WordPress
Unified API - Following IGNY8 API standards
Error Handling - Comprehensive error management and user feedback
Performance - Optimized with client-side processing and memoization
Scalability - Designed for future enhancements

The system is production-ready and fully integrated with the IGNY8 architecture.


Document End