Files
igny8/docs/WRITER_IMAGES_PAGE_SYSTEM_DESIGN.md
alorig 8d096b383a 21
2025-11-29 14:33:07 +05:00

1229 lines
33 KiB
Markdown

# 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](#overview)
2. [Architecture](#architecture)
3. [Component Structure](#component-structure)
4. [State Management](#state-management)
5. [API Functions Chain](#api-functions-chain)
6. [Data Flow & Lifecycle](#data-flow--lifecycle)
7. [WordPress Publishing System](#wordpress-publishing-system)
8. [Automated Publishing](#automated-publishing)
9. [Sync Functions](#sync-functions)
10. [Error Handling](#error-handling)
11. [Performance Optimizations](#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):
```typescript
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
```typescript
// 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
```typescript
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()**
```typescript
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**:
```typescript
{
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()**
```typescript
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**:
```json
{
"ids": [123, 124, 125],
"content_id": 42
}
```
**Response**:
```json
{
"success": true,
"task_id": "celery-task-uuid",
"message": "Image generation started",
"images_created": 3
}
```
#### 3. **bulkUpdateImagesStatus()**
```typescript
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**:
```json
{
"content_id": 42,
"status": "generated"
}
```
**Response**:
```json
{
"updated_count": 4
}
```
#### 4. **fetchImageGenerationSettings()**
```typescript
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**:
```typescript
{
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
```typescript
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**
```sequence
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**
```sequence
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**
```sequence
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**
```sequence
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**
```sequence
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
```typescript
const response = await fetchAPI('/v1/publisher/publish/', {
method: 'POST',
body: JSON.stringify({
content_id: 42, // Content to publish
destinations: ['wordpress'] // Target platform
})
});
```
#### Response Structure
```typescript
{
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
```typescript
// 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**
```typescript
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**
```typescript
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**
```typescript
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
### 4. **Debounced Search**
```typescript
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:
```typescript
{
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
```typescript
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**
```typescript
// 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**
```typescript
// 500ms delay on search to avoid API thrashing
useEffect(() => {
const timer = setTimeout(() => {
loadImages();
}, 500);
return () => clearTimeout(timer);
}, [searchTerm, ...]);
```
### 4. **Lazy Loading (Potential)**
```typescript
// 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`
```typescript
'/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`
```typescript
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
```typescript
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
```typescript
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
```typescript
// In any page using Images component
const resourceDebugEnabled = useResourceDebug();
// Shows AI Function Logs panel with all operations
```
### Check API Responses
```typescript
// 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**