diff --git a/docs/tech-stack/00-SYSTEM-ARCHITECTURE-MASTER-REFERENCE.md b/approved-docs/00-SYSTEM-ARCHITECTURE-MASTER-REFERENCE.md similarity index 100% rename from docs/tech-stack/00-SYSTEM-ARCHITECTURE-MASTER-REFERENCE.md rename to approved-docs/00-SYSTEM-ARCHITECTURE-MASTER-REFERENCE.md diff --git a/docs/API/01-IGNY8-REST-API-COMPLETE-REFERENCE.md b/approved-docs/01-IGNY8-REST-API-COMPLETE-REFERENCE.md similarity index 100% rename from docs/API/01-IGNY8-REST-API-COMPLETE-REFERENCE.md rename to approved-docs/01-IGNY8-REST-API-COMPLETE-REFERENCE.md diff --git a/docs/igny8-app/02-PLANNER-WRITER-WORKFLOW-TECHNICAL-GUIDE.md b/approved-docs/02-PLANNER-WRITER-WORKFLOW-TECHNICAL-GUIDE.md similarity index 100% rename from docs/igny8-app/02-PLANNER-WRITER-WORKFLOW-TECHNICAL-GUIDE.md rename to approved-docs/02-PLANNER-WRITER-WORKFLOW-TECHNICAL-GUIDE.md diff --git a/docs/igny8-app/05-WRITER-IMAGES-PAGE-SYSTEM-DESIGN.md b/approved-docs/05-WRITER-IMAGES-PAGE-SYSTEM-DESIGN.md similarity index 100% rename from docs/igny8-app/05-WRITER-IMAGES-PAGE-SYSTEM-DESIGN.md rename to approved-docs/05-WRITER-IMAGES-PAGE-SYSTEM-DESIGN.md diff --git a/docs/igny8-app/06-FEATURE-MODIFICATION-DEVELOPER-GUIDE.md b/approved-docs/06-FEATURE-MODIFICATION-DEVELOPER-GUIDE.md similarity index 100% rename from docs/igny8-app/06-FEATURE-MODIFICATION-DEVELOPER-GUIDE.md rename to approved-docs/06-FEATURE-MODIFICATION-DEVELOPER-GUIDE.md diff --git a/docs/ai/AI-FUNCTIONS-COMPLETE-REFERENCE.md b/approved-docs/AI-FUNCTIONS-COMPLETE-REFERENCE.md similarity index 100% rename from docs/ai/AI-FUNCTIONS-COMPLETE-REFERENCE.md rename to approved-docs/AI-FUNCTIONS-COMPLETE-REFERENCE.md diff --git a/docs/API/API-COMPLETE-REFERENCE-LATEST.md b/approved-docs/API-COMPLETE-REFERENCE-LATEST.md similarity index 96% rename from docs/API/API-COMPLETE-REFERENCE-LATEST.md rename to approved-docs/API-COMPLETE-REFERENCE-LATEST.md index 8fc3479c..814c263c 100644 --- a/docs/API/API-COMPLETE-REFERENCE-LATEST.md +++ b/approved-docs/API-COMPLETE-REFERENCE-LATEST.md @@ -1,1516 +1,1516 @@ -# IGNY8 API Complete Reference v1.0 - -**Base URL**: `https://api.igny8.com/api/v1/` -**Version**: 1.0.0 -**Last Updated**: 2025-01-XX -**Status**: ✅ **100% IMPLEMENTED** - All endpoints use unified format - -**Purpose**: Complete, unified reference for IGNY8 API covering authentication, endpoints, response formats, error handling, rate limiting, permissions, and integration examples. - ---- - -## Table of Contents - -1. [Quick Start](#quick-start) -2. [Overview & Architecture](#overview--architecture) -3. [Authentication & Authorization](#authentication--authorization) -4. [Response Format Standard](#response-format-standard) -5. [Error Handling](#error-handling) -6. [Rate Limiting](#rate-limiting) -7. [Pagination](#pagination) -8. [Roles & Permissions](#roles--permissions) -9. [Tenant / Site / Sector Scoping](#tenant--site--sector-scoping) -10. [Complete Endpoint Reference](#complete-endpoint-reference) -11. [Integration Examples](#integration-examples) -12. [Testing & Debugging](#testing--debugging) -13. [Change Management](#change-management) - ---- - -## Quick Start - -### Interactive Documentation - -- **Swagger UI**: `https://api.igny8.com/api/docs/` -- **ReDoc**: `https://api.igny8.com/api/redoc/` -- **OpenAPI Schema**: `https://api.igny8.com/api/schema/` - -### Basic Example - -```python -import requests - -BASE_URL = "https://api.igny8.com/api/v1" - -# 1. Login -response = requests.post( - f"{BASE_URL}/auth/login/", - json={"email": "user@example.com", "password": "password"} -) -data = response.json() - -if data['success']: - token = data['data']['access'] - - # 2. Use token for authenticated requests - headers = { - 'Authorization': f'Bearer {token}', - 'Content-Type': 'application/json' - } - - # 3. Get keywords - response = requests.get( - f"{BASE_URL}/planner/keywords/", - headers=headers - ) - result = response.json() - - if result['success']: - keywords = result['results'] # Paginated results - print(f"Found {result['count']} keywords") -``` - ---- - -## Overview & Architecture - -### API Standard v1.0 - Key Principles - -1. **Unified Response Format**: All endpoints return consistent JSON structure -2. **Layered Authorization**: Authentication → Tenant Access → Role → Site/Sector -3. **Centralized Error Handling**: All errors wrapped in unified format -4. **Scoped Rate Limiting**: Different limits for different operation types -5. **Tenant Isolation**: All resources scoped by account/site/sector -6. **Request Tracking**: Every request has a unique ID for debugging - -### Base URL Structure - -``` -Production: https://api.igny8.com/api/v1/ -Development: http://localhost:8000/api/v1/ -``` - -### Module Namespaces - -``` -/api/v1/ -├── auth/ # Authentication and user management -├── account/ # Account settings, team, and usage analytics -├── integration/ # Site integrations and sync -├── system/ # Settings, prompts, integrations -├── admin/billing/ # Admin-only billing management -├── billing/ # Credits, transactions, usage -├── planner/ # Keywords, clusters, content ideas -├── writer/ # Tasks, content, images -├── automation/ # Automation configuration and runs -├── linker/ # Internal linking operations -├── optimizer/ # Content optimization operations -└── publisher/ # Publishing records and deployments -``` - -### Module → Tag Map (Swagger/ReDoc) -- Authentication → `Authentication` -- Account → `Account` -- Integration → `Integration` -- System → `System` -- Admin Billing → `Admin Billing` -- Billing → `Billing` -- Planner → `Planner` -- Writer → `Writer` -- Automation → `Automation` -- Linker → `Linker` -- Optimizer → `Optimizer` -- Publisher → `Publisher` - -Tag display order (docs): Authentication, Account, Integration, System, Admin Billing, Billing, Planner, Writer, Automation, Linker, Optimizer, Publisher - -### Technology Stack - -- **Framework**: Django REST Framework (DRF) -- **Authentication**: JWT Bearer tokens (primary), Session (fallback), Basic (fallback) -- **Pagination**: CustomPageNumberPagination (default: 10, max: 100) -- **Rate Limiting**: Scoped throttles per module/operation type -- **OpenAPI**: drf-spectacular for schema generation - -### Implementation Status - -✅ **100% Complete** - All endpoints implemented with: -- Unified response format -- Proper authentication and authorization -- Rate limiting configured -- Error handling standardized -- Request ID tracking -- Complete Swagger/OpenAPI documentation - ---- - -## Authentication & Authorization - -### Authentication Methods - -#### Primary: JWT Bearer Token - -``` -Authorization: Bearer -``` - -**Token Characteristics:** -- Contains `user_id` and `account_id` -- Type: `access` (15-minute expiry) -- Automatically sets `request.account` via middleware -- Resolves account → tenant context automatically - -**Token Payload:** -```json -{ - "user_id": 1, - "account_id": 1, - "type": "access", - "exp": 1234567890 -} -``` - -#### Fallback Methods - -1. **Session Authentication** (admin panel) - - Class: `CSRFExemptSessionAuthentication` - - Use case: Django admin panel (`/admin/`) - -2. **Basic Authentication** (debug/testing) - - Class: `rest_framework.authentication.BasicAuthentication` - - Use case: API testing tools (Postman, curl) - -### Authentication Order - -1. JWT Token Authentication (tried first) -2. Session Authentication (fallback) -3. Basic Authentication (last fallback) -4. If all fail: 401 Unauthorized - -### Getting an Access Token - -**Login Endpoint:** -```http -POST /api/v1/auth/login/ -Content-Type: application/json - -{ - "email": "user@example.com", - "password": "your_password" -} -``` - -**Response:** -```json -{ - "success": true, - "data": { - "user": { - "id": 1, - "email": "user@example.com", - "username": "user", - "role": "owner", - "account": { ... } - }, - "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", - "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." - }, - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -### Token Expiration - -- **Access Token**: 15 minutes -- **Refresh Token**: 7 days - -**Refresh Token:** -```http -POST /api/v1/auth/refresh/ -Content-Type: application/json - -{ - "refresh": "your_refresh_token" -} -``` - -### Public Endpoints (No Authentication Required) - -- `POST /api/v1/auth/register/` - User registration -- `POST /api/v1/auth/login/` - User login -- `GET /api/v1/auth/plans/` - List plans -- `GET /api/v1/auth/industries/` - List industries -- `GET /api/v1/system/status/` - System health check -- `GET /api/v1/system/ping/` - Health check endpoint - -**All other endpoints require JWT authentication.** - -### Authorization Layers - -Every endpoint enforces layered authorization: - -1. **User Authentication**: User must be authenticated -2. **Tenant Access**: User must belong to the tenant/account -3. **Role Authorization**: User must have appropriate role -4. **Site/Sector Access**: User must have access to requested site/sector - ---- - -## Response Format Standard - -### Mandatory Format - -**This is the global standard for all endpoints - no exceptions.** - -### Success Response - -```json -{ - "success": true, - "data": { - "id": 1, - "name": "Example Keyword", - "status": "active" - }, - "message": "Optional human-readable success message", - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -### Paginated Response - -```json -{ - "success": true, - "count": 120, - "next": "http://api.igny8.com/api/v1/planner/keywords/?page=3", - "previous": "http://api.igny8.com/api/v1/planner/keywords/?page=1", - "results": [ - {"id": 1, "name": "Keyword 1"}, - {"id": 2, "name": "Keyword 2"}, - ... - ], - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -### Error Response - -```json -{ - "success": false, - "error": "Readable top-level error message", - "errors": { - "field_name": ["Field-specific error messages"] - }, - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -### Response Helper Functions - -**File:** `backend/igny8_core/api/response.py` - -```python -from igny8_core.api.response import success_response, error_response, paginated_response - -# Success response -return success_response( - data={"id": 1, "name": "Example"}, - message="Resource created successfully", - status_code=status.HTTP_201_CREATED -) - -# Error response -return error_response( - error="Validation failed", - errors={"email": ["Invalid email format"]}, - status_code=status.HTTP_400_BAD_REQUEST -) - -# Paginated response -paginator = CustomPageNumberPagination() -page = paginator.paginate_queryset(queryset, request) -serializer = MySerializer(page, many=True) -paginated_data = paginator.get_paginated_response(serializer.data).data -return paginated_response(paginated_data, message="Resources retrieved successfully") -``` - ---- - -## Error Handling - -### HTTP Status Codes - -| Code | Meaning | Description | -|------|---------|-------------| -| 200 | OK | Request successful | -| 201 | Created | Resource created successfully | -| 204 | No Content | Resource deleted successfully | -| 400 | Bad Request | Validation error or invalid request | -| 401 | Unauthorized | Authentication required | -| 403 | Forbidden | Permission denied | -| 404 | Not Found | Resource not found | -| 409 | Conflict | Resource conflict (e.g., duplicate) | -| 422 | Unprocessable Entity | Validation failed | -| 429 | Too Many Requests | Rate limit exceeded | -| 500 | Internal Server Error | Server error | - -### Centralized Exception Handler - -**File:** `backend/igny8_core/api/exception_handlers.py` - -All exceptions are handled by a centralized exception handler that: -- Wraps all errors in unified format -- Uses proper HTTP status codes -- Includes sanitized validation errors under `errors` -- Always attaches `request_id` for error tracking -- Logs full exception details -- In DEBUG mode: includes traceback + request context - -### Error Response Examples - -**Validation Error (400):** -```json -{ - "success": false, - "error": "Validation failed", - "errors": { - "email": ["Invalid email format"], - "password": ["Password must be at least 8 characters"] - }, - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -**Authentication Error (401):** -```json -{ - "success": false, - "error": "Authentication required", - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -**Permission Error (403):** -```json -{ - "success": false, - "error": "Permission denied", - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -**Rate Limit (429):** -```json -{ - "success": false, - "error": "Rate limit exceeded", - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -### Server-side Logging - -- All 4xx errors logged as **warning** -- All 5xx errors logged as **error** -- Structured format with timestamp, request_id, endpoint, user_id, account_id, status_code, error_message -- Rotating log files -- Sentry integration hooks for production - ---- - -## Rate Limiting - -### Rate Limiting Configuration - -Rate limits are scoped by operation type. Check response headers for limit information: - -- `X-Throttle-Limit`: Maximum requests allowed -- `X-Throttle-Remaining`: Remaining requests in current window -- `X-Throttle-Reset`: Time when limit resets (Unix timestamp) - -### Rate Limit Scopes - -| Scope | Limit | Description | -|-------|-------|-------------| -| `ai_function` | 10/min | AI content generation, clustering | -| `image_gen` | 15/min | Image generation | -| `content_write` | 30/min | Content creation, updates | -| `content_read` | 100/min | Content listing, retrieval | -| `auth` | 20/min | Login, register, password reset | -| `auth_strict` | 5/min | Sensitive auth operations | -| `planner` | 60/min | Keyword, cluster, idea operations | -| `planner_ai` | 10/min | AI-powered planner operations | -| `writer` | 60/min | Task, content management | -| `writer_ai` | 10/min | AI-powered writer operations | -| `system` | 100/min | Settings, prompts, profiles | -| `system_admin` | 30/min | Admin-only system operations | -| `billing` | 30/min | Credit queries, usage logs | -| `billing_admin` | 10/min | Credit management (admin) | -| `default` | 100/min | Default for endpoints without scope | - -### Handling Rate Limits - -When rate limited (429), the response includes: -- Error message: "Rate limit exceeded" -- Headers with reset time -- Wait until `X-Throttle-Reset` before retrying - -**Example:** -```http -HTTP/1.1 429 Too Many Requests -X-Throttle-Limit: 60 -X-Throttle-Remaining: 0 -X-Throttle-Reset: 1700123456 -Retry-After: 60 - -{ - "success": false, - "error": "Rate limit exceeded", - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -### Debug Mode Bypass - -Set `IGNY8_DEBUG_THROTTLE=True` or `DEBUG=True` to bypass throttling in development. - ---- - -## Pagination - -### Pagination Configuration - -**Default Settings:** -- Default page size: **10** -- Maximum page size: **100** -- Query parameter: `page_size` (optional) -- Page parameter: `page` (default: 1) - -### Query Parameters - -**Pagination:** -``` -?page=2&page_size=25 -``` - -**Filtering:** -``` -?status=active -?site_id=1 -?sector_id=2 -?cluster_id=5 -``` - -**Search:** -``` -?search=keyword -``` - -**Ordering:** -``` -?ordering=-created_at -?ordering=name,status -``` - -### Pagination Response Format - -```json -{ - "success": true, - "count": 150, - "next": "http://api.igny8.com/api/v1/planner/keywords/?page=3&page_size=25", - "previous": "http://api.igny8.com/api/v1/planner/keywords/?page=1&page_size=25", - "results": [ - // Array of results - ], - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -### Pagination Fields - -- `count`: Total number of items -- `next`: URL to next page (null if last page) -- `previous`: URL to previous page (null if first page) -- `results`: Array of items for current page - ---- - -## Roles & Permissions - -### Role Hierarchy - -``` -owner > admin > editor > viewer > system_bot -``` - -### Standard Permission Classes - -**File:** `backend/igny8_core/api/permissions.py` - -| Permission Class | Description | Use Case | -|-----------------|------------|----------| -| `IsAuthenticatedAndActive` | User authenticated and active | Base permission for most endpoints | -| `HasTenantAccess` | User belongs to tenant/account | Tenant isolation | -| `IsViewerOrAbove` | Viewer, editor, admin, or owner | Read-only operations | -| `IsEditorOrAbove` | Editor, admin, or owner | Content operations | -| `IsAdminOrOwner` | Admin or owner only | Settings, keys, billing | - -### Permission Matrix by Endpoint Type - -| Endpoint Type | Required Permissions | Roles Allowed | -|--------------|---------------------|---------------| -| Public (register, login) | `AllowAny` | Anyone | -| Read-only (list, retrieve) | `IsAuthenticatedAndActive` + `HasTenantAccess` | All authenticated users | -| Content operations | `IsAuthenticatedAndActive` + `HasTenantAccess` + `IsEditorOrAbove` | Editor, Admin, Owner | -| User management | `IsAuthenticatedAndActive` + `HasTenantAccess` + `IsAdminOrOwner` | Admin, Owner | -| Billing/Transactions | `IsAuthenticatedAndActive` + `HasTenantAccess` + `IsAdminOrOwner` | Admin, Owner | -| Integration settings | `IsAuthenticatedAndActive` + `HasTenantAccess` + `IsAdminOrOwner` | Admin, Owner | - ---- - -## Tenant / Site / Sector Scoping - -### Scoping Rules - -Every resource created or fetched must be scoped by: - -1. **Account/Tenant** - User's account -2. **Site** - Specific site within account -3. **Sector** - Specific sector within site - -### Enforcement - -**Base Classes:** -- `AccountModelViewSet` - Handles account isolation -- `SiteSectorModelViewSet` - Filters queries by site/sector - -**Requirements:** -- All custom actions must use `.get_queryset()` to avoid bypassing filters -- Any ID list must be verified to belong to the authenticated tenant -- Site/sector access validated based on user role - -### Scoping Example - -```python -class KeywordViewSet(SiteSectorModelViewSet): - # Automatically filters by: - # 1. account (from request.account) - # 2. site_id (from query params or request) - # 3. sector_id (from query params or request) - - queryset = Keyword.objects.all() - serializer_class = KeywordSerializer - - def get_queryset(self): - # Base class handles account/site/sector filtering - queryset = super().get_queryset() - # Additional filtering can be added here - return queryset -``` - ---- - -## Complete Endpoint Reference - -### Authentication Endpoints - -**Base Path**: `/api/v1/auth/` - -#### POST `/api/v1/auth/register/` -**Purpose**: User registration -**Authentication**: None (AllowAny) - -**Request:** -```json -{ - "email": "user@example.com", - "password": "password123", - "password_confirm": "password123" -} -``` - -**Response:** -```json -{ - "success": true, - "data": { - "user": { - "id": 1, - "email": "user@example.com", - "role": "owner", - "account": { ... } - } - }, - "message": "Registration successful" -} -``` - -#### POST `/api/v1/auth/login/` -**Purpose**: User login -**Authentication**: None (AllowAny) - -**Request:** -```json -{ - "email": "user@example.com", - "password": "password123" -} -``` - -**Response:** -```json -{ - "success": true, - "data": { - "user": { ... }, - "access": "eyJ0eXAiOiJKV1QiLCJhbGc...", - "refresh": "eyJ0eXAiOiJKV1QiLCJhbGc..." - }, - "message": "Login successful" -} -``` - -#### POST `/api/v1/auth/refresh/` -**Purpose**: Refresh access token -**Authentication**: None (requires refresh token) - -**Request:** -```json -{ - "refresh": "your_refresh_token" -} -``` - -#### POST `/api/v1/auth/change-password/` -**Purpose**: Change user password -**Authentication**: Required (IsAuthenticated) - -**Request:** -```json -{ - "old_password": "oldpass123", - "new_password": "newpass123", - "new_password_confirm": "newpass123" -} -``` - -#### GET `/api/v1/auth/me/` -**Purpose**: Get current user information -**Authentication**: Required (IsAuthenticated) - -**Response:** -```json -{ - "success": true, - "data": { - "user": { - "id": 1, - "email": "user@example.com", - "role": "owner", - "account": { ... }, - "accessible_sites": [ ... ] - } - } -} -``` - -### User Management Endpoints - -**Base Path**: `/api/v1/auth/users/` -**Permission**: IsOwnerOrAdmin - -**Standard CRUD:** -- `GET /api/v1/auth/users/` - List users -- `POST /api/v1/auth/users/` - Create user -- `GET /api/v1/auth/users/{id}/` - Get user -- `PUT /api/v1/auth/users/{id}/` - Update user -- `DELETE /api/v1/auth/users/{id}/` - Delete user - -**Custom Actions:** -- `POST /api/v1/auth/users/invite/` - Invite user -- `POST /api/v1/auth/users/{id}/activate/` - Activate user - -### Account Management Endpoints - -**Base Path**: `/api/v1/auth/accounts/` -**Permission**: IsOwnerOrAdmin - -**Standard CRUD:** -- `GET /api/v1/auth/accounts/` - List accounts -- `POST /api/v1/auth/accounts/` - Create account -- `GET /api/v1/auth/accounts/{id}/` - Get account -- `PUT /api/v1/auth/accounts/{id}/` - Update account -- `DELETE /api/v1/auth/accounts/{id}/` - Delete account - -### Site Management Endpoints - -**Base Path**: `/api/v1/auth/sites/` -**Permission**: IsEditorOrAbove - -**Standard CRUD:** -- `GET /api/v1/auth/sites/` - List sites -- `POST /api/v1/auth/sites/` - Create site -- `GET /api/v1/auth/sites/{id}/` - Get site -- `PUT /api/v1/auth/sites/{id}/` - Update site -- `DELETE /api/v1/auth/sites/{id}/` - Delete site - -**Custom Actions:** -- `GET /api/v1/auth/sites/{id}/sectors/` - Get site sectors -- `POST /api/v1/auth/sites/{id}/set_active/` - Set active site -- `POST /api/v1/auth/sites/{id}/select_sectors/` - Select sectors - -### Sector Management Endpoints - -**Base Path**: `/api/v1/auth/sectors/` -**Permission**: IsEditorOrAbove - -**Standard CRUD:** -- `GET /api/v1/auth/sectors/` - List sectors -- `POST /api/v1/auth/sectors/` - Create sector -- `GET /api/v1/auth/sectors/{id}/` - Get sector -- `PUT /api/v1/auth/sectors/{id}/` - Update sector -- `DELETE /api/v1/auth/sectors/{id}/` - Delete sector - -### Planner Module Endpoints - -**Base Path**: `/api/v1/planner/` - -#### Keyword Management - -**Base Path**: `/api/v1/planner/keywords/` -**Permission**: IsAuthenticatedAndActive + HasTenantAccess -**Inherits**: SiteSectorModelViewSet - -**Standard CRUD:** -- `GET /api/v1/planner/keywords/` - List keywords (paginated) -- `POST /api/v1/planner/keywords/` - Create keyword -- `GET /api/v1/planner/keywords/{id}/` - Get keyword -- `PUT /api/v1/planner/keywords/{id}/` - Update keyword -- `DELETE /api/v1/planner/keywords/{id}/` - Delete keyword - -**Filtering:** -- `status` - Filter by status -- `cluster_id` - Filter by cluster -- `seed_keyword__intent` - Filter by intent -- `seed_keyword_id` - Filter by seed keyword ID -- `difficulty_min`, `difficulty_max` - Difficulty range -- `volume_min`, `volume_max` - Volume range -- `site_id` - Filter by site (query param) -- `sector_id` - Filter by sector (query param) - -**Search:** -- `search` - Search by keyword text - -**Ordering:** -- `ordering` - Order by `created_at`, `seed_keyword__volume`, `seed_keyword__difficulty` -- Default: `-created_at` (newest first) - -**Custom Actions:** -- `POST /api/v1/planner/keywords/bulk_delete/` - Bulk delete keywords - - Request: `{ "ids": [1, 2, 3] }` -- `POST /api/v1/planner/keywords/bulk_update_status/` - Bulk update status - - Request: `{ "ids": [1, 2, 3], "status": "active" }` -- `POST /api/v1/planner/keywords/bulk_add_from_seed/` - Add keywords from seed library - - Request: `{ "seed_keyword_ids": [1, 2, 3], "site_id": 1, "sector_id": 1 }` -- `GET /api/v1/planner/keywords/export/` - Export keywords to CSV -- `POST /api/v1/planner/keywords/import_keywords/` - Import keywords from CSV -- `POST /api/v1/planner/keywords/auto_cluster/` - Auto-cluster keywords using AI - - Request: `{ "ids": [1, 2, 3, ...], "sector_id": 1 }` - - Max Keywords: 20 per batch - - Returns: Celery task ID for progress tracking - -#### Cluster Management - -**Base Path**: `/api/v1/planner/clusters/` -**Permission**: IsAuthenticatedAndActive + HasTenantAccess -**Inherits**: SiteSectorModelViewSet - -**Standard CRUD:** -- `GET /api/v1/planner/clusters/` - List clusters -- `POST /api/v1/planner/clusters/` - Create cluster -- `GET /api/v1/planner/clusters/{id}/` - Get cluster -- `PUT /api/v1/planner/clusters/{id}/` - Update cluster -- `DELETE /api/v1/planner/clusters/{id}/` - Delete cluster - -**Filtering:** -- `status` - Filter by status -- `site_id` - Filter by site (query param) -- `sector_id` - Filter by sector (query param) - -**Search:** -- `search` - Search by cluster name - -**Custom Actions:** -- `POST /api/v1/planner/clusters/bulk_delete/` - Bulk delete clusters -- `POST /api/v1/planner/clusters/auto_generate_ideas/` - Auto-generate content ideas - - Request: `{ "ids": [1] }` (max 1 cluster per batch) - - Returns: Celery task ID for progress tracking - -#### Content Ideas Management - -**Base Path**: `/api/v1/planner/ideas/` -**Permission**: IsAuthenticatedAndActive + HasTenantAccess -**Inherits**: SiteSectorModelViewSet - -**Standard CRUD:** -- `GET /api/v1/planner/ideas/` - List content ideas -- `POST /api/v1/planner/ideas/` - Create content idea -- `GET /api/v1/planner/ideas/{id}/` - Get content idea -- `PUT /api/v1/planner/ideas/{id}/` - Update content idea -- `DELETE /api/v1/planner/ideas/{id}/` - Delete content idea - -**Filtering:** -- `status` - Filter by status -- `cluster_id` - Filter by cluster -- `content_type` - Filter by content type -- `site_id` - Filter by site (query param) -- `sector_id` - Filter by sector (query param) - -**Custom Actions:** -- `POST /api/v1/planner/ideas/bulk_delete/` - Bulk delete ideas -- `POST /api/v1/planner/ideas/bulk_queue_to_writer/` - Queue ideas to writer (create tasks) - - Request: `{ "ids": [1, 2, 3] }` - -### Writer Module Endpoints - -**Base Path**: `/api/v1/writer/` - -#### Task Management - -**Base Path**: `/api/v1/writer/tasks/` -**Permission**: IsAuthenticatedAndActive + HasTenantAccess -**Inherits**: SiteSectorModelViewSet - -**Standard CRUD:** -- `GET /api/v1/writer/tasks/` - List tasks -- `POST /api/v1/writer/tasks/` - Create task -- `GET /api/v1/writer/tasks/{id}/` - Get task -- `PUT /api/v1/writer/tasks/{id}/` - Update task -- `DELETE /api/v1/writer/tasks/{id}/` - Delete task - -**Filtering:** -- `status` - Filter by status (draft, in_progress, review, completed, archived) -- `cluster_id` - Filter by cluster -- `content_type` - Filter by content type -- `content_structure` - Filter by content structure -- `site_id` - Filter by site (query param) -- `sector_id` - Filter by sector (query param) - -**Search:** -- `search` - Search by title or keywords - -**Custom Actions:** -- `POST /api/v1/writer/tasks/bulk_delete/` - Bulk delete tasks -- `POST /api/v1/writer/tasks/bulk_update/` - Bulk update task status -- `POST /api/v1/writer/tasks/auto_generate_content/` - Auto-generate content using AI - - Request: `{ "ids": [1, 2, 3, ...] }` (max 50 tasks per batch) - - Returns: Celery task ID for progress tracking - -#### Content Management - -**Base Path**: `/api/v1/writer/content/` -**Permission**: IsAuthenticatedAndActive + HasTenantAccess -**Inherits**: SiteSectorModelViewSet - -**Standard CRUD:** -- `GET /api/v1/writer/content/` - List content -- `POST /api/v1/writer/content/` - Create content -- `GET /api/v1/writer/content/{id}/` - Get content -- `PUT /api/v1/writer/content/{id}/` - Update content -- `DELETE /api/v1/writer/content/{id}/` - Delete content - -**Filtering:** -- `status` - Filter by status -- `content_type` - Filter by content type -- `site_id` - Filter by site (query param) -- `sector_id` - Filter by sector (query param) - -**Custom Actions:** -- `POST /api/v1/writer/content/generate_image_prompts/` - Generate image prompts from content - - Request: `{ "ids": [1, 2, 3] }` - - Returns: Celery task ID for progress tracking - -#### Image Management - -**Base Path**: `/api/v1/writer/images/` -**Permission**: IsAuthenticatedAndActive + HasTenantAccess -**Inherits**: SiteSectorModelViewSet - -**Standard CRUD:** -- `GET /api/v1/writer/images/` - List images -- `POST /api/v1/writer/images/` - Create image -- `GET /api/v1/writer/images/{id}/` - Get image -- `PUT /api/v1/writer/images/{id}/` - Update image -- `DELETE /api/v1/writer/images/{id}/` - Delete image - -**Filtering:** -- `image_type` - Filter by type (featured, in_article, desktop, mobile) -- `status` - Filter by status -- `content_id` - Filter by content -- `task_id` - Filter by task -- `site_id` - Filter by site (query param) -- `sector_id` - Filter by sector (query param) - -**Custom Actions:** -- `GET /api/v1/writer/images/{id}/file/` - Get image file URL -- `GET /api/v1/writer/images/content_images/` - Get images for content - - Query Params: `content_id` (required) -- `POST /api/v1/writer/images/generate_images/` - Generate images using AI - - Request: `{ "ids": [1, 2, 3, ...] }` - - Returns: Celery task ID for progress tracking -- `POST /api/v1/writer/images/bulk_update/` - Bulk update image status - -### System Module Endpoints - -**Base Path**: `/api/v1/system/` - -#### AI Prompt Management - -**Base Path**: `/api/v1/system/prompts/` -**Permission**: IsAuthenticatedAndActive + HasTenantAccess -**Inherits**: AccountModelViewSet - -**Standard CRUD:** -- `GET /api/v1/system/prompts/` - List prompts -- `POST /api/v1/system/prompts/` - Create prompt -- `GET /api/v1/system/prompts/{id}/` - Get prompt -- `PUT /api/v1/system/prompts/{id}/` - Update prompt -- `DELETE /api/v1/system/prompts/{id}/` - Delete prompt - -**Custom Actions:** -- `GET /api/v1/system/prompts/by_type/{prompt_type}/` - Get prompt by type -- `POST /api/v1/system/prompts/save/` - Save prompt (requires editor/admin) -- `POST /api/v1/system/prompts/reset/` - Reset prompt to default - -#### Integration Settings - -**Base Path**: `/api/v1/system/settings/integrations/` -**Permission**: IsAdminOrOwner - -**Custom URL Patterns:** -- `GET /api/v1/system/settings/integrations/{pk}/` - Get integration settings -- `POST /api/v1/system/settings/integrations/{pk}/save/` - Save integration settings -- `PUT /api/v1/system/settings/integrations/{pk}/` - Update integration settings -- `POST /api/v1/system/settings/integrations/{pk}/test/` - Test connection - - Request: `{ "provider": "openai" }` or `{ "provider": "runware" }` -- `POST /api/v1/system/settings/integrations/{pk}/generate/` - Test image generation -- `GET /api/v1/system/settings/task_progress/{task_id}/` - Get Celery task progress -- `GET /api/v1/system/integrations/image_generation/` - Get image generation settings - -#### System Status - -**Base Path**: `/api/v1/system/` - -- `GET /api/v1/system/status/` - System health check (AllowAny) -- `GET /api/v1/system/ping/` - Health check endpoint (AllowAny) -- `GET /api/v1/system/request-metrics/{request_id}/` - Get request metrics for debugging - -### Admin Billing & Credits (Admin-only, Unified) - -**Base Path**: `/api/v1/admin/billing/` (all admin billing/credits live here) - -- `GET /api/v1/admin/billing/stats/` - System billing stats (admin-only) -- `GET /api/v1/admin/billing/invoices/` - Admin invoice listing (all accounts) -- `GET /api/v1/admin/billing/payments/` - Admin payment listing (all accounts) -- `GET /api/v1/admin/billing/pending_payments/` - Pending manual payments (admin review queue) -- `POST /api/v1/admin/billing/{id}/approve_payment/` - Approve manual payment (admin-only) -- `POST /api/v1/admin/billing/{id}/reject_payment/` - Reject manual payment (admin-only) - -- `GET /api/v1/admin/billing/payment-method-configs/` - List payment method configs (country-level); query: `country_code`, `payment_method` -- `POST /api/v1/admin/billing/payment-method-configs/` - Create payment method config -- `GET /api/v1/admin/billing/payment-method-configs/{id}/` - Retrieve payment method config -- `PATCH/PUT /api/v1/admin/billing/payment-method-configs/{id}/` - Update payment method config -- `DELETE /api/v1/admin/billing/payment-method-configs/{id}/` - Delete payment method config -- `GET /api/v1/admin/billing/account-payment-methods/` - List account payment methods (query `account_id` to scope) -- `POST /api/v1/admin/billing/account-payment-methods/` - Create account payment method -- `GET /api/v1/admin/billing/account-payment-methods/{id}/` - Retrieve account payment method -- `PATCH/PUT /api/v1/admin/billing/account-payment-methods/{id}/` - Update account payment method -- `DELETE /api/v1/admin/billing/account-payment-methods/{id}/` - Delete account payment method -- `POST /api/v1/admin/billing/account-payment-methods/{id}/set_default/` - Set default account payment method -- `GET /api/v1/admin/credit-costs/` - List credit cost configurations (admin-only) -- `POST /api/v1/admin/credit-costs/` - Update credit cost configurations (admin-only) -- `GET /api/v1/admin/users/` - List users/accounts with credit info (admin-only) -- `POST /api/v1/admin/users/{user_id}/adjust-credits/` - Adjust user credits (admin-only) - -> Non-standard/legacy endpoints to deprecate and remove (do not use): -> - `/api/v1/billing/admin/stats/` -> - `/api/v1/billing/admin/invoices/` -> - `/api/v1/billing/admin/payments/` -> - `/api/v1/billing/admin/pending_payments/` -> - `/api/v1/billing/admin/{id}/approve_payment/` -> - `/api/v1/billing/admin/{id}/reject_payment/` - - -### Billing Module Endpoints - -**Base Path**: `/api/v1/billing/` - -#### Credit Balance - -**Base Path**: `/api/v1/billing/credits/balance/` -**Permission**: IsAuthenticatedAndActive + HasTenantAccess - -- `GET /api/v1/billing/credits/balance/` - Get credit balance - -**Response:** -```json -{ - "success": true, - "data": { - "credits": 1000, - "plan_credits_per_month": 500, - "credits_used_this_month": 250, - "credits_remaining": 750 - } -} -``` - -#### Credit Usage - -**Base Path**: `/api/v1/billing/credits/usage/` -**Permission**: IsAuthenticatedAndActive + HasTenantAccess - -**Standard CRUD:** -- `GET /api/v1/billing/credits/usage/` - List usage logs (paginated) -- `GET /api/v1/billing/credits/usage/{id}/` - Get usage log - -**Filtering:** -- `operation_type` - Filter by operation type -- `start_date` - Filter by start date (YYYY-MM-DD) -- `end_date` - Filter by end date (YYYY-MM-DD) - -**Custom Actions:** -- `GET /api/v1/billing/credits/usage/summary/` - Get usage summary -- `GET /api/v1/billing/credits/usage/limits/` - Get usage limits - -#### Credit Transactions - -**Base Path**: `/api/v1/billing/credits/transactions/` -**Permission**: IsAdminOrOwner - -**Standard CRUD:** -- `GET /api/v1/billing/credits/transactions/` - List transactions (paginated) -- `GET /api/v1/billing/credits/transactions/{id}/` - Get transaction - -**Filtering:** -- `transaction_type` - Filter by type -- `start_date` - Filter by start date -- `end_date` - Filter by end date - -#### Credit Packages - -**Base Path**: `/api/v1/billing/credit-packages/` -**Permission**: IsAuthenticated - -- `GET /api/v1/billing/credit-packages/` - List available credit packages -- `POST /api/v1/billing/credit-packages/{id}/purchase/` - Purchase a credit package - -#### Invoices - -**Base Path**: `/api/v1/billing/invoices/` -**Permission**: IsAuthenticated - -- `GET /api/v1/billing/invoices/` - List invoices -- `GET /api/v1/billing/invoices/{id}/` - Get invoice detail -- `GET /api/v1/billing/invoices/{id}/download_pdf/` - Download invoice PDF - -#### Payment Methods - -**Base Path**: `/api/v1/billing/payment-methods/` -**Permission**: IsAuthenticated - -- `GET /api/v1/billing/payment-methods/` - List payment methods -- `POST /api/v1/billing/payment-methods/` - Create payment method -- `GET /api/v1/billing/payment-methods/{id}/` - Get payment method -- `PUT /api/v1/billing/payment-methods/{id}/` - Update payment method -- `PATCH /api/v1/billing/payment-methods/{id}/` - Partial update payment method -- `DELETE /api/v1/billing/payment-methods/{id}/` - Delete payment method -- `POST /api/v1/billing/payment-methods/{id}/set_default/` - Set default payment method -- `GET /api/v1/billing/payment-methods/available/` - List available payment methods (config-driven) - -#### Payments - -**Base Path**: `/api/v1/billing/payments/` -**Permission**: IsAuthenticated - -- `GET /api/v1/billing/payments/` - List payments -- `POST /api/v1/billing/payments/manual/` - Submit manual payment for approval - -#### Transactions (alias-free) - -**Base Path**: `/api/v1/billing/transactions/` -**Permission**: IsAuthenticated - -- `GET /api/v1/billing/transactions/` - List transactions (with current balance included) - ---- - -## Integration Examples - -### Python - -```python -import requests - -BASE_URL = "https://api.igny8.com/api/v1" - -# Login -response = requests.post( - f"{BASE_URL}/auth/login/", - json={"email": "user@example.com", "password": "password"} -) -data = response.json() - -if data['success']: - token = data['data']['access'] - - # Use token for authenticated requests - headers = { - 'Authorization': f'Bearer {token}', - 'Content-Type': 'application/json' - } - - # Get keywords - response = requests.get( - f"{BASE_URL}/planner/keywords/?page=1&page_size=25", - headers=headers - ) - result = response.json() - - if result['success']: - keywords = result['results'] - print(f"Found {result['count']} keywords") - else: - print(f"Error: {result['error']}") -``` - -### JavaScript - -```javascript -const BASE_URL = 'https://api.igny8.com/api/v1'; - -// Login -const loginResponse = await fetch(`${BASE_URL}/auth/login/`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - email: 'user@example.com', - password: 'password' - }) -}); - -const loginData = await loginResponse.json(); - -if (loginData.success) { - const token = loginData.data.access; - - // Use token for authenticated requests - const headers = { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }; - - // Get keywords - const keywordsResponse = await fetch( - `${BASE_URL}/planner/keywords/?page=1&page_size=25`, - { headers } - ); - - const keywordsData = await keywordsResponse.json(); - - if (keywordsData.success) { - const keywords = keywordsData.results; - console.log(`Found ${keywordsData.count} keywords`); - } else { - console.error('Error:', keywordsData.error); - } -} -``` - -### cURL - -```bash -# Login -curl -X POST https://api.igny8.com/api/v1/auth/login/ \ - -H "Content-Type: application/json" \ - -d '{"email":"user@example.com","password":"password"}' - -# Get keywords (with token) -curl -X GET https://api.igny8.com/api/v1/planner/keywords/ \ - -H "Authorization: Bearer YOUR_TOKEN" \ - -H "Content-Type: application/json" -``` - -### Frontend Integration (TypeScript) - -```typescript -// Using fetchAPI wrapper (automatically extracts data from unified format) -import { fetchAPI } from './services/api'; - -// List keywords -const response = await fetchAPI('/v1/planner/keywords/?page=1&page_size=25'); -// response is already extracted: { count, next, previous, results } - -// Create keyword -const newKeyword = await fetchAPI('/v1/planner/keywords/', { - method: 'POST', - body: JSON.stringify({ - keyword: 'example keyword', - site_id: 1, - sector_id: 2, - status: 'active' - }) -}); - -// Error handling -try { - const data = await fetchAPI('/v1/planner/keywords/'); -} catch (error) { - // Error is already parsed from unified format - console.error(error.message); - if (error.errors) { - // Handle field-specific errors - Object.keys(error.errors).forEach(field => { - console.error(`${field}: ${error.errors[field].join(', ')}`); - }); - } -} -``` - -### WordPress Plugin Integration (PHP) - -```php -class Igny8API { - private $base_url = 'https://api.igny8.com/api/v1'; - private $token; - - public function login($email, $password) { - $response = wp_remote_post($this->base_url . '/auth/login/', [ - 'headers' => ['Content-Type' => 'application/json'], - 'body' => json_encode(['email' => $email, 'password' => $password]) - ]); - - $data = json_decode(wp_remote_retrieve_body($response), true); - - if ($data['success']) { - $this->token = $data['data']['access']; - return true; - } - - return false; - } - - public function getKeywords($page = 1, $page_size = 25) { - $response = wp_remote_get( - $this->base_url . '/planner/keywords/?page=' . $page . '&page_size=' . $page_size, - [ - 'headers' => [ - 'Authorization' => 'Bearer ' . $this->token, - 'Content-Type' => 'application/json' - ] - ] - ); - - $data = json_decode(wp_remote_retrieve_body($response), true); - - if ($data['success']) { - return $data['results']; - } - - return []; - } -} -``` - ---- - -## Testing & Debugging - -### Request ID Tracking - -Every API request includes a unique `request_id` in the response. Use this ID for: -- Debugging issues -- Log correlation -- Support requests - -The `request_id` is included in: -- All success responses -- All error responses -- Response headers (`X-Request-ID`) - -### Progress Tracking (AI Functions) - -All AI functions return a Celery task ID for progress tracking: - -**Request:** -```json -POST /api/v1/planner/keywords/auto_cluster/ -{ - "ids": [1, 2, 3, 4, 5], - "sector_id": 1 -} -``` - -**Response:** -```json -{ - "success": true, - "data": { - "task_id": "abc123-def456-ghi789" - } -} -``` - -**Poll Progress:** -```json -GET /api/v1/system/settings/task_progress/abc123-def456-ghi789/ -``` - -**Progress Response:** -```json -{ - "state": "PROGRESS", - "meta": { - "phase": "AI_CALL", - "percentage": 50, - "message": "Processing keywords...", - "request_steps": [...], - "response_steps": [...], - "cost": 0.05, - "tokens": 1500 - } -} -``` - -### Error Handling Best Practices - -**401 Unauthorized**: Trigger logout → redirect to login -**403 Forbidden**: Show permission alert -**429 Too Many Requests**: Show rate limit warning with retry time -**4xx/5xx**: Display error message from `error` field - -### Rate Limit Monitoring - -```typescript -const throttleLimit = response.headers.get('X-Throttle-Limit'); -const throttleRemaining = response.headers.get('X-Throttle-Remaining'); - -if (parseInt(throttleRemaining) < 5) { - showNotification('Approaching rate limit', 'warning'); -} -``` - ---- - -## Change Management - -### Versioning Strategy - -- **v1** remains stable long-term -- Breaking changes require **v2** -- Deprecations allowed only with explicit timeline -- Non-breaking changes can be added to v1 - -### Breaking Changes - -**Definition:** Changes that require client code updates - -**Examples:** -- Removing an endpoint -- Changing response structure -- Changing authentication method -- Removing a field from response - -**Process:** -1. Document breaking change -2. Provide migration guide -3. Deprecate in v1 with timeline -4. Implement in v2 -5. Maintain v1 for deprecation period - -### Non-Breaking Changes - -**Definition:** Changes that don't require client code updates - -**Examples:** -- Adding new endpoints -- Adding optional fields to response -- Adding new query parameters -- Performance improvements - -### Changelog - -All API changes are documented in `CHANGELOG.md` with: -- Version number -- Date -- Type (Added, Changed, Fixed, Deprecated, Removed, Security) -- Description -- Affected areas -- Migration notes (if applicable) - -### Endpoint Change & Documentation Checklist (Unified API) - -1) Design -- Map to an existing module/tag; if new, add to Module Namespaces and Tag Map. -- Choose path under the correct base (`/api/v1/{module}/...`); avoid new sub-namespaces unless justified. - -2) Implement -- Use unified response helpers and proper permissions/rate limits. -- Add `extend_schema` tags matching the module tag. - -3) Schema & Docs -- Ensure swagger tag exists in `SPECTACULAR_SETTINGS` with the agreed order. -- Regenerate/reload the API (server restart) so `/api/schema/` reflects changes. -- Verify in Swagger UI (`/api/docs/`) and ReDoc (`/api/redoc/`) that the operation is under the right tag. - -4) Reference Updates -- Update this reference file with the new endpoint(s) under the correct module section. -- Update `CHANGELOG.md` (type, summary, impacted clients). - -5) Deprecation (if applicable) -- Mark legacy routes, add timeline, and keep compatibility shims only temporarily. - ---- - -## Summary - -### Implementation Status - -✅ **100% Complete** - All endpoints implemented with: -- Unified response format (`{success, data, message, errors, request_id}`) -- Proper authentication and authorization (JWT Bearer tokens) -- Rate limiting configured (scoped by operation type) -- Error handling standardized (centralized exception handler) -- Request ID tracking (every request has unique ID) -- Complete Swagger/OpenAPI documentation -- Tenant/site/sector scoping (automatic filtering) -- Pagination standardized (default: 10, max: 100) - -### Key Features - -- **100+ endpoints** across 5 modules -- **Unified response format** for all endpoints -- **Scoped rate limiting** (10-100 requests/minute depending on operation) -- **Layered authorization** (Authentication → Tenant → Role → Site/Sector) -- **Complete OpenAPI documentation** (Swagger UI, ReDoc) -- **Request ID tracking** for debugging -- **Progress tracking** for AI functions -- **Comprehensive error handling** with clear messages - -### Access Points - -- **Interactive Documentation**: `https://api.igny8.com/api/docs/` -- **ReDoc**: `https://api.igny8.com/api/redoc/` -- **OpenAPI Schema**: `https://api.igny8.com/api/schema/` - ---- - -**Last Updated**: 2025-01-XX -**API Version**: 1.0.0 -**Status**: ✅ **100% IMPLEMENTED** - +# IGNY8 API Complete Reference v1.0 + +**Base URL**: `https://api.igny8.com/api/v1/` +**Version**: 1.0.0 +**Last Updated**: 2025-01-XX +**Status**: ✅ **100% IMPLEMENTED** - All endpoints use unified format + +**Purpose**: Complete, unified reference for IGNY8 API covering authentication, endpoints, response formats, error handling, rate limiting, permissions, and integration examples. + +--- + +## Table of Contents + +1. [Quick Start](#quick-start) +2. [Overview & Architecture](#overview--architecture) +3. [Authentication & Authorization](#authentication--authorization) +4. [Response Format Standard](#response-format-standard) +5. [Error Handling](#error-handling) +6. [Rate Limiting](#rate-limiting) +7. [Pagination](#pagination) +8. [Roles & Permissions](#roles--permissions) +9. [Tenant / Site / Sector Scoping](#tenant--site--sector-scoping) +10. [Complete Endpoint Reference](#complete-endpoint-reference) +11. [Integration Examples](#integration-examples) +12. [Testing & Debugging](#testing--debugging) +13. [Change Management](#change-management) + +--- + +## Quick Start + +### Interactive Documentation + +- **Swagger UI**: `https://api.igny8.com/api/docs/` +- **ReDoc**: `https://api.igny8.com/api/redoc/` +- **OpenAPI Schema**: `https://api.igny8.com/api/schema/` + +### Basic Example + +```python +import requests + +BASE_URL = "https://api.igny8.com/api/v1" + +# 1. Login +response = requests.post( + f"{BASE_URL}/auth/login/", + json={"email": "user@example.com", "password": "password"} +) +data = response.json() + +if data['success']: + token = data['data']['access'] + + # 2. Use token for authenticated requests + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + + # 3. Get keywords + response = requests.get( + f"{BASE_URL}/planner/keywords/", + headers=headers + ) + result = response.json() + + if result['success']: + keywords = result['results'] # Paginated results + print(f"Found {result['count']} keywords") +``` + +--- + +## Overview & Architecture + +### API Standard v1.0 - Key Principles + +1. **Unified Response Format**: All endpoints return consistent JSON structure +2. **Layered Authorization**: Authentication → Tenant Access → Role → Site/Sector +3. **Centralized Error Handling**: All errors wrapped in unified format +4. **Scoped Rate Limiting**: Different limits for different operation types +5. **Tenant Isolation**: All resources scoped by account/site/sector +6. **Request Tracking**: Every request has a unique ID for debugging + +### Base URL Structure + +``` +Production: https://api.igny8.com/api/v1/ +Development: http://localhost:8000/api/v1/ +``` + +### Module Namespaces + +``` +/api/v1/ +├── auth/ # Authentication and user management +├── account/ # Account settings, team, and usage analytics +├── integration/ # Site integrations and sync +├── system/ # Settings, prompts, integrations +├── admin/billing/ # Admin-only billing management +├── billing/ # Credits, transactions, usage +├── planner/ # Keywords, clusters, content ideas +├── writer/ # Tasks, content, images +├── automation/ # Automation configuration and runs +├── linker/ # Internal linking operations +├── optimizer/ # Content optimization operations +└── publisher/ # Publishing records and deployments +``` + +### Module → Tag Map (Swagger/ReDoc) +- Authentication → `Authentication` +- Account → `Account` +- Integration → `Integration` +- System → `System` +- Admin Billing → `Admin Billing` +- Billing → `Billing` +- Planner → `Planner` +- Writer → `Writer` +- Automation → `Automation` +- Linker → `Linker` +- Optimizer → `Optimizer` +- Publisher → `Publisher` + +Tag display order (docs): Authentication, Account, Integration, System, Admin Billing, Billing, Planner, Writer, Automation, Linker, Optimizer, Publisher + +### Technology Stack + +- **Framework**: Django REST Framework (DRF) +- **Authentication**: JWT Bearer tokens (primary), Session (fallback), Basic (fallback) +- **Pagination**: CustomPageNumberPagination (default: 10, max: 100) +- **Rate Limiting**: Scoped throttles per module/operation type +- **OpenAPI**: drf-spectacular for schema generation + +### Implementation Status + +✅ **100% Complete** - All endpoints implemented with: +- Unified response format +- Proper authentication and authorization +- Rate limiting configured +- Error handling standardized +- Request ID tracking +- Complete Swagger/OpenAPI documentation + +--- + +## Authentication & Authorization + +### Authentication Methods + +#### Primary: JWT Bearer Token + +``` +Authorization: Bearer +``` + +**Token Characteristics:** +- Contains `user_id` and `account_id` +- Type: `access` (15-minute expiry) +- Automatically sets `request.account` via middleware +- Resolves account → tenant context automatically + +**Token Payload:** +```json +{ + "user_id": 1, + "account_id": 1, + "type": "access", + "exp": 1234567890 +} +``` + +#### Fallback Methods + +1. **Session Authentication** (admin panel) + - Class: `CSRFExemptSessionAuthentication` + - Use case: Django admin panel (`/admin/`) + +2. **Basic Authentication** (debug/testing) + - Class: `rest_framework.authentication.BasicAuthentication` + - Use case: API testing tools (Postman, curl) + +### Authentication Order + +1. JWT Token Authentication (tried first) +2. Session Authentication (fallback) +3. Basic Authentication (last fallback) +4. If all fail: 401 Unauthorized + +### Getting an Access Token + +**Login Endpoint:** +```http +POST /api/v1/auth/login/ +Content-Type: application/json + +{ + "email": "user@example.com", + "password": "your_password" +} +``` + +**Response:** +```json +{ + "success": true, + "data": { + "user": { + "id": 1, + "email": "user@example.com", + "username": "user", + "role": "owner", + "account": { ... } + }, + "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + }, + "request_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +### Token Expiration + +- **Access Token**: 15 minutes +- **Refresh Token**: 7 days + +**Refresh Token:** +```http +POST /api/v1/auth/refresh/ +Content-Type: application/json + +{ + "refresh": "your_refresh_token" +} +``` + +### Public Endpoints (No Authentication Required) + +- `POST /api/v1/auth/register/` - User registration +- `POST /api/v1/auth/login/` - User login +- `GET /api/v1/auth/plans/` - List plans +- `GET /api/v1/auth/industries/` - List industries +- `GET /api/v1/system/status/` - System health check +- `GET /api/v1/system/ping/` - Health check endpoint + +**All other endpoints require JWT authentication.** + +### Authorization Layers + +Every endpoint enforces layered authorization: + +1. **User Authentication**: User must be authenticated +2. **Tenant Access**: User must belong to the tenant/account +3. **Role Authorization**: User must have appropriate role +4. **Site/Sector Access**: User must have access to requested site/sector + +--- + +## Response Format Standard + +### Mandatory Format + +**This is the global standard for all endpoints - no exceptions.** + +### Success Response + +```json +{ + "success": true, + "data": { + "id": 1, + "name": "Example Keyword", + "status": "active" + }, + "message": "Optional human-readable success message", + "request_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +### Paginated Response + +```json +{ + "success": true, + "count": 120, + "next": "http://api.igny8.com/api/v1/planner/keywords/?page=3", + "previous": "http://api.igny8.com/api/v1/planner/keywords/?page=1", + "results": [ + {"id": 1, "name": "Keyword 1"}, + {"id": 2, "name": "Keyword 2"}, + ... + ], + "request_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +### Error Response + +```json +{ + "success": false, + "error": "Readable top-level error message", + "errors": { + "field_name": ["Field-specific error messages"] + }, + "request_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +### Response Helper Functions + +**File:** `backend/igny8_core/api/response.py` + +```python +from igny8_core.api.response import success_response, error_response, paginated_response + +# Success response +return success_response( + data={"id": 1, "name": "Example"}, + message="Resource created successfully", + status_code=status.HTTP_201_CREATED +) + +# Error response +return error_response( + error="Validation failed", + errors={"email": ["Invalid email format"]}, + status_code=status.HTTP_400_BAD_REQUEST +) + +# Paginated response +paginator = CustomPageNumberPagination() +page = paginator.paginate_queryset(queryset, request) +serializer = MySerializer(page, many=True) +paginated_data = paginator.get_paginated_response(serializer.data).data +return paginated_response(paginated_data, message="Resources retrieved successfully") +``` + +--- + +## Error Handling + +### HTTP Status Codes + +| Code | Meaning | Description | +|------|---------|-------------| +| 200 | OK | Request successful | +| 201 | Created | Resource created successfully | +| 204 | No Content | Resource deleted successfully | +| 400 | Bad Request | Validation error or invalid request | +| 401 | Unauthorized | Authentication required | +| 403 | Forbidden | Permission denied | +| 404 | Not Found | Resource not found | +| 409 | Conflict | Resource conflict (e.g., duplicate) | +| 422 | Unprocessable Entity | Validation failed | +| 429 | Too Many Requests | Rate limit exceeded | +| 500 | Internal Server Error | Server error | + +### Centralized Exception Handler + +**File:** `backend/igny8_core/api/exception_handlers.py` + +All exceptions are handled by a centralized exception handler that: +- Wraps all errors in unified format +- Uses proper HTTP status codes +- Includes sanitized validation errors under `errors` +- Always attaches `request_id` for error tracking +- Logs full exception details +- In DEBUG mode: includes traceback + request context + +### Error Response Examples + +**Validation Error (400):** +```json +{ + "success": false, + "error": "Validation failed", + "errors": { + "email": ["Invalid email format"], + "password": ["Password must be at least 8 characters"] + }, + "request_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +**Authentication Error (401):** +```json +{ + "success": false, + "error": "Authentication required", + "request_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +**Permission Error (403):** +```json +{ + "success": false, + "error": "Permission denied", + "request_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +**Rate Limit (429):** +```json +{ + "success": false, + "error": "Rate limit exceeded", + "request_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +### Server-side Logging + +- All 4xx errors logged as **warning** +- All 5xx errors logged as **error** +- Structured format with timestamp, request_id, endpoint, user_id, account_id, status_code, error_message +- Rotating log files +- Sentry integration hooks for production + +--- + +## Rate Limiting + +### Rate Limiting Configuration + +Rate limits are scoped by operation type. Check response headers for limit information: + +- `X-Throttle-Limit`: Maximum requests allowed +- `X-Throttle-Remaining`: Remaining requests in current window +- `X-Throttle-Reset`: Time when limit resets (Unix timestamp) + +### Rate Limit Scopes + +| Scope | Limit | Description | +|-------|-------|-------------| +| `ai_function` | 10/min | AI content generation, clustering | +| `image_gen` | 15/min | Image generation | +| `content_write` | 30/min | Content creation, updates | +| `content_read` | 100/min | Content listing, retrieval | +| `auth` | 20/min | Login, register, password reset | +| `auth_strict` | 5/min | Sensitive auth operations | +| `planner` | 60/min | Keyword, cluster, idea operations | +| `planner_ai` | 10/min | AI-powered planner operations | +| `writer` | 60/min | Task, content management | +| `writer_ai` | 10/min | AI-powered writer operations | +| `system` | 100/min | Settings, prompts, profiles | +| `system_admin` | 30/min | Admin-only system operations | +| `billing` | 30/min | Credit queries, usage logs | +| `billing_admin` | 10/min | Credit management (admin) | +| `default` | 100/min | Default for endpoints without scope | + +### Handling Rate Limits + +When rate limited (429), the response includes: +- Error message: "Rate limit exceeded" +- Headers with reset time +- Wait until `X-Throttle-Reset` before retrying + +**Example:** +```http +HTTP/1.1 429 Too Many Requests +X-Throttle-Limit: 60 +X-Throttle-Remaining: 0 +X-Throttle-Reset: 1700123456 +Retry-After: 60 + +{ + "success": false, + "error": "Rate limit exceeded", + "request_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +### Debug Mode Bypass + +Set `IGNY8_DEBUG_THROTTLE=True` or `DEBUG=True` to bypass throttling in development. + +--- + +## Pagination + +### Pagination Configuration + +**Default Settings:** +- Default page size: **10** +- Maximum page size: **100** +- Query parameter: `page_size` (optional) +- Page parameter: `page` (default: 1) + +### Query Parameters + +**Pagination:** +``` +?page=2&page_size=25 +``` + +**Filtering:** +``` +?status=active +?site_id=1 +?sector_id=2 +?cluster_id=5 +``` + +**Search:** +``` +?search=keyword +``` + +**Ordering:** +``` +?ordering=-created_at +?ordering=name,status +``` + +### Pagination Response Format + +```json +{ + "success": true, + "count": 150, + "next": "http://api.igny8.com/api/v1/planner/keywords/?page=3&page_size=25", + "previous": "http://api.igny8.com/api/v1/planner/keywords/?page=1&page_size=25", + "results": [ + // Array of results + ], + "request_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +### Pagination Fields + +- `count`: Total number of items +- `next`: URL to next page (null if last page) +- `previous`: URL to previous page (null if first page) +- `results`: Array of items for current page + +--- + +## Roles & Permissions + +### Role Hierarchy + +``` +owner > admin > editor > viewer > system_bot +``` + +### Standard Permission Classes + +**File:** `backend/igny8_core/api/permissions.py` + +| Permission Class | Description | Use Case | +|-----------------|------------|----------| +| `IsAuthenticatedAndActive` | User authenticated and active | Base permission for most endpoints | +| `HasTenantAccess` | User belongs to tenant/account | Tenant isolation | +| `IsViewerOrAbove` | Viewer, editor, admin, or owner | Read-only operations | +| `IsEditorOrAbove` | Editor, admin, or owner | Content operations | +| `IsAdminOrOwner` | Admin or owner only | Settings, keys, billing | + +### Permission Matrix by Endpoint Type + +| Endpoint Type | Required Permissions | Roles Allowed | +|--------------|---------------------|---------------| +| Public (register, login) | `AllowAny` | Anyone | +| Read-only (list, retrieve) | `IsAuthenticatedAndActive` + `HasTenantAccess` | All authenticated users | +| Content operations | `IsAuthenticatedAndActive` + `HasTenantAccess` + `IsEditorOrAbove` | Editor, Admin, Owner | +| User management | `IsAuthenticatedAndActive` + `HasTenantAccess` + `IsAdminOrOwner` | Admin, Owner | +| Billing/Transactions | `IsAuthenticatedAndActive` + `HasTenantAccess` + `IsAdminOrOwner` | Admin, Owner | +| Integration settings | `IsAuthenticatedAndActive` + `HasTenantAccess` + `IsAdminOrOwner` | Admin, Owner | + +--- + +## Tenant / Site / Sector Scoping + +### Scoping Rules + +Every resource created or fetched must be scoped by: + +1. **Account/Tenant** - User's account +2. **Site** - Specific site within account +3. **Sector** - Specific sector within site + +### Enforcement + +**Base Classes:** +- `AccountModelViewSet` - Handles account isolation +- `SiteSectorModelViewSet` - Filters queries by site/sector + +**Requirements:** +- All custom actions must use `.get_queryset()` to avoid bypassing filters +- Any ID list must be verified to belong to the authenticated tenant +- Site/sector access validated based on user role + +### Scoping Example + +```python +class KeywordViewSet(SiteSectorModelViewSet): + # Automatically filters by: + # 1. account (from request.account) + # 2. site_id (from query params or request) + # 3. sector_id (from query params or request) + + queryset = Keyword.objects.all() + serializer_class = KeywordSerializer + + def get_queryset(self): + # Base class handles account/site/sector filtering + queryset = super().get_queryset() + # Additional filtering can be added here + return queryset +``` + +--- + +## Complete Endpoint Reference + +### Authentication Endpoints + +**Base Path**: `/api/v1/auth/` + +#### POST `/api/v1/auth/register/` +**Purpose**: User registration +**Authentication**: None (AllowAny) + +**Request:** +```json +{ + "email": "user@example.com", + "password": "password123", + "password_confirm": "password123" +} +``` + +**Response:** +```json +{ + "success": true, + "data": { + "user": { + "id": 1, + "email": "user@example.com", + "role": "owner", + "account": { ... } + } + }, + "message": "Registration successful" +} +``` + +#### POST `/api/v1/auth/login/` +**Purpose**: User login +**Authentication**: None (AllowAny) + +**Request:** +```json +{ + "email": "user@example.com", + "password": "password123" +} +``` + +**Response:** +```json +{ + "success": true, + "data": { + "user": { ... }, + "access": "eyJ0eXAiOiJKV1QiLCJhbGc...", + "refresh": "eyJ0eXAiOiJKV1QiLCJhbGc..." + }, + "message": "Login successful" +} +``` + +#### POST `/api/v1/auth/refresh/` +**Purpose**: Refresh access token +**Authentication**: None (requires refresh token) + +**Request:** +```json +{ + "refresh": "your_refresh_token" +} +``` + +#### POST `/api/v1/auth/change-password/` +**Purpose**: Change user password +**Authentication**: Required (IsAuthenticated) + +**Request:** +```json +{ + "old_password": "oldpass123", + "new_password": "newpass123", + "new_password_confirm": "newpass123" +} +``` + +#### GET `/api/v1/auth/me/` +**Purpose**: Get current user information +**Authentication**: Required (IsAuthenticated) + +**Response:** +```json +{ + "success": true, + "data": { + "user": { + "id": 1, + "email": "user@example.com", + "role": "owner", + "account": { ... }, + "accessible_sites": [ ... ] + } + } +} +``` + +### User Management Endpoints + +**Base Path**: `/api/v1/auth/users/` +**Permission**: IsOwnerOrAdmin + +**Standard CRUD:** +- `GET /api/v1/auth/users/` - List users +- `POST /api/v1/auth/users/` - Create user +- `GET /api/v1/auth/users/{id}/` - Get user +- `PUT /api/v1/auth/users/{id}/` - Update user +- `DELETE /api/v1/auth/users/{id}/` - Delete user + +**Custom Actions:** +- `POST /api/v1/auth/users/invite/` - Invite user +- `POST /api/v1/auth/users/{id}/activate/` - Activate user + +### Account Management Endpoints + +**Base Path**: `/api/v1/auth/accounts/` +**Permission**: IsOwnerOrAdmin + +**Standard CRUD:** +- `GET /api/v1/auth/accounts/` - List accounts +- `POST /api/v1/auth/accounts/` - Create account +- `GET /api/v1/auth/accounts/{id}/` - Get account +- `PUT /api/v1/auth/accounts/{id}/` - Update account +- `DELETE /api/v1/auth/accounts/{id}/` - Delete account + +### Site Management Endpoints + +**Base Path**: `/api/v1/auth/sites/` +**Permission**: IsEditorOrAbove + +**Standard CRUD:** +- `GET /api/v1/auth/sites/` - List sites +- `POST /api/v1/auth/sites/` - Create site +- `GET /api/v1/auth/sites/{id}/` - Get site +- `PUT /api/v1/auth/sites/{id}/` - Update site +- `DELETE /api/v1/auth/sites/{id}/` - Delete site + +**Custom Actions:** +- `GET /api/v1/auth/sites/{id}/sectors/` - Get site sectors +- `POST /api/v1/auth/sites/{id}/set_active/` - Set active site +- `POST /api/v1/auth/sites/{id}/select_sectors/` - Select sectors + +### Sector Management Endpoints + +**Base Path**: `/api/v1/auth/sectors/` +**Permission**: IsEditorOrAbove + +**Standard CRUD:** +- `GET /api/v1/auth/sectors/` - List sectors +- `POST /api/v1/auth/sectors/` - Create sector +- `GET /api/v1/auth/sectors/{id}/` - Get sector +- `PUT /api/v1/auth/sectors/{id}/` - Update sector +- `DELETE /api/v1/auth/sectors/{id}/` - Delete sector + +### Planner Module Endpoints + +**Base Path**: `/api/v1/planner/` + +#### Keyword Management + +**Base Path**: `/api/v1/planner/keywords/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess +**Inherits**: SiteSectorModelViewSet + +**Standard CRUD:** +- `GET /api/v1/planner/keywords/` - List keywords (paginated) +- `POST /api/v1/planner/keywords/` - Create keyword +- `GET /api/v1/planner/keywords/{id}/` - Get keyword +- `PUT /api/v1/planner/keywords/{id}/` - Update keyword +- `DELETE /api/v1/planner/keywords/{id}/` - Delete keyword + +**Filtering:** +- `status` - Filter by status +- `cluster_id` - Filter by cluster +- `seed_keyword__intent` - Filter by intent +- `seed_keyword_id` - Filter by seed keyword ID +- `difficulty_min`, `difficulty_max` - Difficulty range +- `volume_min`, `volume_max` - Volume range +- `site_id` - Filter by site (query param) +- `sector_id` - Filter by sector (query param) + +**Search:** +- `search` - Search by keyword text + +**Ordering:** +- `ordering` - Order by `created_at`, `seed_keyword__volume`, `seed_keyword__difficulty` +- Default: `-created_at` (newest first) + +**Custom Actions:** +- `POST /api/v1/planner/keywords/bulk_delete/` - Bulk delete keywords + - Request: `{ "ids": [1, 2, 3] }` +- `POST /api/v1/planner/keywords/bulk_update_status/` - Bulk update status + - Request: `{ "ids": [1, 2, 3], "status": "active" }` +- `POST /api/v1/planner/keywords/bulk_add_from_seed/` - Add keywords from seed library + - Request: `{ "seed_keyword_ids": [1, 2, 3], "site_id": 1, "sector_id": 1 }` +- `GET /api/v1/planner/keywords/export/` - Export keywords to CSV +- `POST /api/v1/planner/keywords/import_keywords/` - Import keywords from CSV +- `POST /api/v1/planner/keywords/auto_cluster/` - Auto-cluster keywords using AI + - Request: `{ "ids": [1, 2, 3, ...], "sector_id": 1 }` + - Max Keywords: 20 per batch + - Returns: Celery task ID for progress tracking + +#### Cluster Management + +**Base Path**: `/api/v1/planner/clusters/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess +**Inherits**: SiteSectorModelViewSet + +**Standard CRUD:** +- `GET /api/v1/planner/clusters/` - List clusters +- `POST /api/v1/planner/clusters/` - Create cluster +- `GET /api/v1/planner/clusters/{id}/` - Get cluster +- `PUT /api/v1/planner/clusters/{id}/` - Update cluster +- `DELETE /api/v1/planner/clusters/{id}/` - Delete cluster + +**Filtering:** +- `status` - Filter by status +- `site_id` - Filter by site (query param) +- `sector_id` - Filter by sector (query param) + +**Search:** +- `search` - Search by cluster name + +**Custom Actions:** +- `POST /api/v1/planner/clusters/bulk_delete/` - Bulk delete clusters +- `POST /api/v1/planner/clusters/auto_generate_ideas/` - Auto-generate content ideas + - Request: `{ "ids": [1] }` (max 1 cluster per batch) + - Returns: Celery task ID for progress tracking + +#### Content Ideas Management + +**Base Path**: `/api/v1/planner/ideas/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess +**Inherits**: SiteSectorModelViewSet + +**Standard CRUD:** +- `GET /api/v1/planner/ideas/` - List content ideas +- `POST /api/v1/planner/ideas/` - Create content idea +- `GET /api/v1/planner/ideas/{id}/` - Get content idea +- `PUT /api/v1/planner/ideas/{id}/` - Update content idea +- `DELETE /api/v1/planner/ideas/{id}/` - Delete content idea + +**Filtering:** +- `status` - Filter by status +- `cluster_id` - Filter by cluster +- `content_type` - Filter by content type +- `site_id` - Filter by site (query param) +- `sector_id` - Filter by sector (query param) + +**Custom Actions:** +- `POST /api/v1/planner/ideas/bulk_delete/` - Bulk delete ideas +- `POST /api/v1/planner/ideas/bulk_queue_to_writer/` - Queue ideas to writer (create tasks) + - Request: `{ "ids": [1, 2, 3] }` + +### Writer Module Endpoints + +**Base Path**: `/api/v1/writer/` + +#### Task Management + +**Base Path**: `/api/v1/writer/tasks/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess +**Inherits**: SiteSectorModelViewSet + +**Standard CRUD:** +- `GET /api/v1/writer/tasks/` - List tasks +- `POST /api/v1/writer/tasks/` - Create task +- `GET /api/v1/writer/tasks/{id}/` - Get task +- `PUT /api/v1/writer/tasks/{id}/` - Update task +- `DELETE /api/v1/writer/tasks/{id}/` - Delete task + +**Filtering:** +- `status` - Filter by status (draft, in_progress, review, completed, archived) +- `cluster_id` - Filter by cluster +- `content_type` - Filter by content type +- `content_structure` - Filter by content structure +- `site_id` - Filter by site (query param) +- `sector_id` - Filter by sector (query param) + +**Search:** +- `search` - Search by title or keywords + +**Custom Actions:** +- `POST /api/v1/writer/tasks/bulk_delete/` - Bulk delete tasks +- `POST /api/v1/writer/tasks/bulk_update/` - Bulk update task status +- `POST /api/v1/writer/tasks/auto_generate_content/` - Auto-generate content using AI + - Request: `{ "ids": [1, 2, 3, ...] }` (max 50 tasks per batch) + - Returns: Celery task ID for progress tracking + +#### Content Management + +**Base Path**: `/api/v1/writer/content/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess +**Inherits**: SiteSectorModelViewSet + +**Standard CRUD:** +- `GET /api/v1/writer/content/` - List content +- `POST /api/v1/writer/content/` - Create content +- `GET /api/v1/writer/content/{id}/` - Get content +- `PUT /api/v1/writer/content/{id}/` - Update content +- `DELETE /api/v1/writer/content/{id}/` - Delete content + +**Filtering:** +- `status` - Filter by status +- `content_type` - Filter by content type +- `site_id` - Filter by site (query param) +- `sector_id` - Filter by sector (query param) + +**Custom Actions:** +- `POST /api/v1/writer/content/generate_image_prompts/` - Generate image prompts from content + - Request: `{ "ids": [1, 2, 3] }` + - Returns: Celery task ID for progress tracking + +#### Image Management + +**Base Path**: `/api/v1/writer/images/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess +**Inherits**: SiteSectorModelViewSet + +**Standard CRUD:** +- `GET /api/v1/writer/images/` - List images +- `POST /api/v1/writer/images/` - Create image +- `GET /api/v1/writer/images/{id}/` - Get image +- `PUT /api/v1/writer/images/{id}/` - Update image +- `DELETE /api/v1/writer/images/{id}/` - Delete image + +**Filtering:** +- `image_type` - Filter by type (featured, in_article, desktop, mobile) +- `status` - Filter by status +- `content_id` - Filter by content +- `task_id` - Filter by task +- `site_id` - Filter by site (query param) +- `sector_id` - Filter by sector (query param) + +**Custom Actions:** +- `GET /api/v1/writer/images/{id}/file/` - Get image file URL +- `GET /api/v1/writer/images/content_images/` - Get images for content + - Query Params: `content_id` (required) +- `POST /api/v1/writer/images/generate_images/` - Generate images using AI + - Request: `{ "ids": [1, 2, 3, ...] }` + - Returns: Celery task ID for progress tracking +- `POST /api/v1/writer/images/bulk_update/` - Bulk update image status + +### System Module Endpoints + +**Base Path**: `/api/v1/system/` + +#### AI Prompt Management + +**Base Path**: `/api/v1/system/prompts/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess +**Inherits**: AccountModelViewSet + +**Standard CRUD:** +- `GET /api/v1/system/prompts/` - List prompts +- `POST /api/v1/system/prompts/` - Create prompt +- `GET /api/v1/system/prompts/{id}/` - Get prompt +- `PUT /api/v1/system/prompts/{id}/` - Update prompt +- `DELETE /api/v1/system/prompts/{id}/` - Delete prompt + +**Custom Actions:** +- `GET /api/v1/system/prompts/by_type/{prompt_type}/` - Get prompt by type +- `POST /api/v1/system/prompts/save/` - Save prompt (requires editor/admin) +- `POST /api/v1/system/prompts/reset/` - Reset prompt to default + +#### Integration Settings + +**Base Path**: `/api/v1/system/settings/integrations/` +**Permission**: IsAdminOrOwner + +**Custom URL Patterns:** +- `GET /api/v1/system/settings/integrations/{pk}/` - Get integration settings +- `POST /api/v1/system/settings/integrations/{pk}/save/` - Save integration settings +- `PUT /api/v1/system/settings/integrations/{pk}/` - Update integration settings +- `POST /api/v1/system/settings/integrations/{pk}/test/` - Test connection + - Request: `{ "provider": "openai" }` or `{ "provider": "runware" }` +- `POST /api/v1/system/settings/integrations/{pk}/generate/` - Test image generation +- `GET /api/v1/system/settings/task_progress/{task_id}/` - Get Celery task progress +- `GET /api/v1/system/integrations/image_generation/` - Get image generation settings + +#### System Status + +**Base Path**: `/api/v1/system/` + +- `GET /api/v1/system/status/` - System health check (AllowAny) +- `GET /api/v1/system/ping/` - Health check endpoint (AllowAny) +- `GET /api/v1/system/request-metrics/{request_id}/` - Get request metrics for debugging + +### Admin Billing & Credits (Admin-only, Unified) + +**Base Path**: `/api/v1/admin/billing/` (all admin billing/credits live here) + +- `GET /api/v1/admin/billing/stats/` - System billing stats (admin-only) +- `GET /api/v1/admin/billing/invoices/` - Admin invoice listing (all accounts) +- `GET /api/v1/admin/billing/payments/` - Admin payment listing (all accounts) +- `GET /api/v1/admin/billing/pending_payments/` - Pending manual payments (admin review queue) +- `POST /api/v1/admin/billing/{id}/approve_payment/` - Approve manual payment (admin-only) +- `POST /api/v1/admin/billing/{id}/reject_payment/` - Reject manual payment (admin-only) + +- `GET /api/v1/admin/billing/payment-method-configs/` - List payment method configs (country-level); query: `country_code`, `payment_method` +- `POST /api/v1/admin/billing/payment-method-configs/` - Create payment method config +- `GET /api/v1/admin/billing/payment-method-configs/{id}/` - Retrieve payment method config +- `PATCH/PUT /api/v1/admin/billing/payment-method-configs/{id}/` - Update payment method config +- `DELETE /api/v1/admin/billing/payment-method-configs/{id}/` - Delete payment method config +- `GET /api/v1/admin/billing/account-payment-methods/` - List account payment methods (query `account_id` to scope) +- `POST /api/v1/admin/billing/account-payment-methods/` - Create account payment method +- `GET /api/v1/admin/billing/account-payment-methods/{id}/` - Retrieve account payment method +- `PATCH/PUT /api/v1/admin/billing/account-payment-methods/{id}/` - Update account payment method +- `DELETE /api/v1/admin/billing/account-payment-methods/{id}/` - Delete account payment method +- `POST /api/v1/admin/billing/account-payment-methods/{id}/set_default/` - Set default account payment method +- `GET /api/v1/admin/credit-costs/` - List credit cost configurations (admin-only) +- `POST /api/v1/admin/credit-costs/` - Update credit cost configurations (admin-only) +- `GET /api/v1/admin/users/` - List users/accounts with credit info (admin-only) +- `POST /api/v1/admin/users/{user_id}/adjust-credits/` - Adjust user credits (admin-only) + +> Non-standard/legacy endpoints to deprecate and remove (do not use): +> - `/api/v1/billing/admin/stats/` +> - `/api/v1/billing/admin/invoices/` +> - `/api/v1/billing/admin/payments/` +> - `/api/v1/billing/admin/pending_payments/` +> - `/api/v1/billing/admin/{id}/approve_payment/` +> - `/api/v1/billing/admin/{id}/reject_payment/` + + +### Billing Module Endpoints + +**Base Path**: `/api/v1/billing/` + +#### Credit Balance + +**Base Path**: `/api/v1/billing/credits/balance/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess + +- `GET /api/v1/billing/credits/balance/` - Get credit balance + +**Response:** +```json +{ + "success": true, + "data": { + "credits": 1000, + "plan_credits_per_month": 500, + "credits_used_this_month": 250, + "credits_remaining": 750 + } +} +``` + +#### Credit Usage + +**Base Path**: `/api/v1/billing/credits/usage/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess + +**Standard CRUD:** +- `GET /api/v1/billing/credits/usage/` - List usage logs (paginated) +- `GET /api/v1/billing/credits/usage/{id}/` - Get usage log + +**Filtering:** +- `operation_type` - Filter by operation type +- `start_date` - Filter by start date (YYYY-MM-DD) +- `end_date` - Filter by end date (YYYY-MM-DD) + +**Custom Actions:** +- `GET /api/v1/billing/credits/usage/summary/` - Get usage summary +- `GET /api/v1/billing/credits/usage/limits/` - Get usage limits + +#### Credit Transactions + +**Base Path**: `/api/v1/billing/credits/transactions/` +**Permission**: IsAdminOrOwner + +**Standard CRUD:** +- `GET /api/v1/billing/credits/transactions/` - List transactions (paginated) +- `GET /api/v1/billing/credits/transactions/{id}/` - Get transaction + +**Filtering:** +- `transaction_type` - Filter by type +- `start_date` - Filter by start date +- `end_date` - Filter by end date + +#### Credit Packages + +**Base Path**: `/api/v1/billing/credit-packages/` +**Permission**: IsAuthenticated + +- `GET /api/v1/billing/credit-packages/` - List available credit packages +- `POST /api/v1/billing/credit-packages/{id}/purchase/` - Purchase a credit package + +#### Invoices + +**Base Path**: `/api/v1/billing/invoices/` +**Permission**: IsAuthenticated + +- `GET /api/v1/billing/invoices/` - List invoices +- `GET /api/v1/billing/invoices/{id}/` - Get invoice detail +- `GET /api/v1/billing/invoices/{id}/download_pdf/` - Download invoice PDF + +#### Payment Methods + +**Base Path**: `/api/v1/billing/payment-methods/` +**Permission**: IsAuthenticated + +- `GET /api/v1/billing/payment-methods/` - List payment methods +- `POST /api/v1/billing/payment-methods/` - Create payment method +- `GET /api/v1/billing/payment-methods/{id}/` - Get payment method +- `PUT /api/v1/billing/payment-methods/{id}/` - Update payment method +- `PATCH /api/v1/billing/payment-methods/{id}/` - Partial update payment method +- `DELETE /api/v1/billing/payment-methods/{id}/` - Delete payment method +- `POST /api/v1/billing/payment-methods/{id}/set_default/` - Set default payment method +- `GET /api/v1/billing/payment-methods/available/` - List available payment methods (config-driven) + +#### Payments + +**Base Path**: `/api/v1/billing/payments/` +**Permission**: IsAuthenticated + +- `GET /api/v1/billing/payments/` - List payments +- `POST /api/v1/billing/payments/manual/` - Submit manual payment for approval + +#### Transactions (alias-free) + +**Base Path**: `/api/v1/billing/transactions/` +**Permission**: IsAuthenticated + +- `GET /api/v1/billing/transactions/` - List transactions (with current balance included) + +--- + +## Integration Examples + +### Python + +```python +import requests + +BASE_URL = "https://api.igny8.com/api/v1" + +# Login +response = requests.post( + f"{BASE_URL}/auth/login/", + json={"email": "user@example.com", "password": "password"} +) +data = response.json() + +if data['success']: + token = data['data']['access'] + + # Use token for authenticated requests + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + + # Get keywords + response = requests.get( + f"{BASE_URL}/planner/keywords/?page=1&page_size=25", + headers=headers + ) + result = response.json() + + if result['success']: + keywords = result['results'] + print(f"Found {result['count']} keywords") + else: + print(f"Error: {result['error']}") +``` + +### JavaScript + +```javascript +const BASE_URL = 'https://api.igny8.com/api/v1'; + +// Login +const loginResponse = await fetch(`${BASE_URL}/auth/login/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email: 'user@example.com', + password: 'password' + }) +}); + +const loginData = await loginResponse.json(); + +if (loginData.success) { + const token = loginData.data.access; + + // Use token for authenticated requests + const headers = { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }; + + // Get keywords + const keywordsResponse = await fetch( + `${BASE_URL}/planner/keywords/?page=1&page_size=25`, + { headers } + ); + + const keywordsData = await keywordsResponse.json(); + + if (keywordsData.success) { + const keywords = keywordsData.results; + console.log(`Found ${keywordsData.count} keywords`); + } else { + console.error('Error:', keywordsData.error); + } +} +``` + +### cURL + +```bash +# Login +curl -X POST https://api.igny8.com/api/v1/auth/login/ \ + -H "Content-Type: application/json" \ + -d '{"email":"user@example.com","password":"password"}' + +# Get keywords (with token) +curl -X GET https://api.igny8.com/api/v1/planner/keywords/ \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "Content-Type: application/json" +``` + +### Frontend Integration (TypeScript) + +```typescript +// Using fetchAPI wrapper (automatically extracts data from unified format) +import { fetchAPI } from './services/api'; + +// List keywords +const response = await fetchAPI('/v1/planner/keywords/?page=1&page_size=25'); +// response is already extracted: { count, next, previous, results } + +// Create keyword +const newKeyword = await fetchAPI('/v1/planner/keywords/', { + method: 'POST', + body: JSON.stringify({ + keyword: 'example keyword', + site_id: 1, + sector_id: 2, + status: 'active' + }) +}); + +// Error handling +try { + const data = await fetchAPI('/v1/planner/keywords/'); +} catch (error) { + // Error is already parsed from unified format + console.error(error.message); + if (error.errors) { + // Handle field-specific errors + Object.keys(error.errors).forEach(field => { + console.error(`${field}: ${error.errors[field].join(', ')}`); + }); + } +} +``` + +### WordPress Plugin Integration (PHP) + +```php +class Igny8API { + private $base_url = 'https://api.igny8.com/api/v1'; + private $token; + + public function login($email, $password) { + $response = wp_remote_post($this->base_url . '/auth/login/', [ + 'headers' => ['Content-Type' => 'application/json'], + 'body' => json_encode(['email' => $email, 'password' => $password]) + ]); + + $data = json_decode(wp_remote_retrieve_body($response), true); + + if ($data['success']) { + $this->token = $data['data']['access']; + return true; + } + + return false; + } + + public function getKeywords($page = 1, $page_size = 25) { + $response = wp_remote_get( + $this->base_url . '/planner/keywords/?page=' . $page . '&page_size=' . $page_size, + [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $this->token, + 'Content-Type' => 'application/json' + ] + ] + ); + + $data = json_decode(wp_remote_retrieve_body($response), true); + + if ($data['success']) { + return $data['results']; + } + + return []; + } +} +``` + +--- + +## Testing & Debugging + +### Request ID Tracking + +Every API request includes a unique `request_id` in the response. Use this ID for: +- Debugging issues +- Log correlation +- Support requests + +The `request_id` is included in: +- All success responses +- All error responses +- Response headers (`X-Request-ID`) + +### Progress Tracking (AI Functions) + +All AI functions return a Celery task ID for progress tracking: + +**Request:** +```json +POST /api/v1/planner/keywords/auto_cluster/ +{ + "ids": [1, 2, 3, 4, 5], + "sector_id": 1 +} +``` + +**Response:** +```json +{ + "success": true, + "data": { + "task_id": "abc123-def456-ghi789" + } +} +``` + +**Poll Progress:** +```json +GET /api/v1/system/settings/task_progress/abc123-def456-ghi789/ +``` + +**Progress Response:** +```json +{ + "state": "PROGRESS", + "meta": { + "phase": "AI_CALL", + "percentage": 50, + "message": "Processing keywords...", + "request_steps": [...], + "response_steps": [...], + "cost": 0.05, + "tokens": 1500 + } +} +``` + +### Error Handling Best Practices + +**401 Unauthorized**: Trigger logout → redirect to login +**403 Forbidden**: Show permission alert +**429 Too Many Requests**: Show rate limit warning with retry time +**4xx/5xx**: Display error message from `error` field + +### Rate Limit Monitoring + +```typescript +const throttleLimit = response.headers.get('X-Throttle-Limit'); +const throttleRemaining = response.headers.get('X-Throttle-Remaining'); + +if (parseInt(throttleRemaining) < 5) { + showNotification('Approaching rate limit', 'warning'); +} +``` + +--- + +## Change Management + +### Versioning Strategy + +- **v1** remains stable long-term +- Breaking changes require **v2** +- Deprecations allowed only with explicit timeline +- Non-breaking changes can be added to v1 + +### Breaking Changes + +**Definition:** Changes that require client code updates + +**Examples:** +- Removing an endpoint +- Changing response structure +- Changing authentication method +- Removing a field from response + +**Process:** +1. Document breaking change +2. Provide migration guide +3. Deprecate in v1 with timeline +4. Implement in v2 +5. Maintain v1 for deprecation period + +### Non-Breaking Changes + +**Definition:** Changes that don't require client code updates + +**Examples:** +- Adding new endpoints +- Adding optional fields to response +- Adding new query parameters +- Performance improvements + +### Changelog + +All API changes are documented in `CHANGELOG.md` with: +- Version number +- Date +- Type (Added, Changed, Fixed, Deprecated, Removed, Security) +- Description +- Affected areas +- Migration notes (if applicable) + +### Endpoint Change & Documentation Checklist (Unified API) + +1) Design +- Map to an existing module/tag; if new, add to Module Namespaces and Tag Map. +- Choose path under the correct base (`/api/v1/{module}/...`); avoid new sub-namespaces unless justified. + +2) Implement +- Use unified response helpers and proper permissions/rate limits. +- Add `extend_schema` tags matching the module tag. + +3) Schema & Docs +- Ensure swagger tag exists in `SPECTACULAR_SETTINGS` with the agreed order. +- Regenerate/reload the API (server restart) so `/api/schema/` reflects changes. +- Verify in Swagger UI (`/api/docs/`) and ReDoc (`/api/redoc/`) that the operation is under the right tag. + +4) Reference Updates +- Update this reference file with the new endpoint(s) under the correct module section. +- Update `CHANGELOG.md` (type, summary, impacted clients). + +5) Deprecation (if applicable) +- Mark legacy routes, add timeline, and keep compatibility shims only temporarily. + +--- + +## Summary + +### Implementation Status + +✅ **100% Complete** - All endpoints implemented with: +- Unified response format (`{success, data, message, errors, request_id}`) +- Proper authentication and authorization (JWT Bearer tokens) +- Rate limiting configured (scoped by operation type) +- Error handling standardized (centralized exception handler) +- Request ID tracking (every request has unique ID) +- Complete Swagger/OpenAPI documentation +- Tenant/site/sector scoping (automatic filtering) +- Pagination standardized (default: 10, max: 100) + +### Key Features + +- **100+ endpoints** across 5 modules +- **Unified response format** for all endpoints +- **Scoped rate limiting** (10-100 requests/minute depending on operation) +- **Layered authorization** (Authentication → Tenant → Role → Site/Sector) +- **Complete OpenAPI documentation** (Swagger UI, ReDoc) +- **Request ID tracking** for debugging +- **Progress tracking** for AI functions +- **Comprehensive error handling** with clear messages + +### Access Points + +- **Interactive Documentation**: `https://api.igny8.com/api/docs/` +- **ReDoc**: `https://api.igny8.com/api/redoc/` +- **OpenAPI Schema**: `https://api.igny8.com/api/schema/` + +--- + +**Last Updated**: 2025-01-XX +**API Version**: 1.0.0 +**Status**: ✅ **100% IMPLEMENTED** + diff --git a/docs/igny8-app/KEYWORDS-CLUSTERS-IDEAS-COMPLETE-MAPPING.md b/approved-docs/KEYWORDS-CLUSTERS-IDEAS-COMPLETE-MAPPING.md similarity index 100% rename from docs/igny8-app/KEYWORDS-CLUSTERS-IDEAS-COMPLETE-MAPPING.md rename to approved-docs/KEYWORDS-CLUSTERS-IDEAS-COMPLETE-MAPPING.md diff --git a/docs/igny8-app/TAXONOMY/TAXONOMY-RELATIONSHIP-DIAGRAM.md b/approved-docs/TAXONOMY-RELATIONSHIP-DIAGRAM.md similarity index 100% rename from docs/igny8-app/TAXONOMY/TAXONOMY-RELATIONSHIP-DIAGRAM.md rename to approved-docs/TAXONOMY-RELATIONSHIP-DIAGRAM.md diff --git a/docs/igny8-app/app-packaging-backaup-plan.md b/approved-docs/app-packaging-backaup-plan.md similarity index 100% rename from docs/igny8-app/app-packaging-backaup-plan.md rename to approved-docs/app-packaging-backaup-plan.md diff --git a/user-flow-plan-in-progress-1.md b/approved-docs/user-flow-plan-in-progress-1.md similarity index 100% rename from user-flow-plan-in-progress-1.md rename to approved-docs/user-flow-plan-in-progress-1.md diff --git a/user-flow-plan-in-progress-2.md b/approved-docs/user-flow-plan-in-progress-2.md similarity index 100% rename from user-flow-plan-in-progress-2.md rename to approved-docs/user-flow-plan-in-progress-2.md diff --git a/user-flow-plan.md b/approved-docs/user-flow-plan.md similarity index 100% rename from user-flow-plan.md rename to approved-docs/user-flow-plan.md diff --git a/old-docs/API/01-IGNY8-REST-API-COMPLETE-REFERENCE.md b/old-docs/API/01-IGNY8-REST-API-COMPLETE-REFERENCE.md new file mode 100644 index 00000000..453459ce --- /dev/null +++ b/old-docs/API/01-IGNY8-REST-API-COMPLETE-REFERENCE.md @@ -0,0 +1,1707 @@ +# IGNY8 API Complete Reference v1.0 + +**Base URL**: `https://api.igny8.com/api/v1/` +**Version**: 1.0.0 +**Last Updated**: 2025-12-03 (Removed deprecated Site Builder module) +**Status**: ✅ **100% IMPLEMENTED** - All endpoints use unified format + +**Purpose**: Complete, unified reference for IGNY8 API covering authentication, endpoints, response formats, error handling, rate limiting, permissions, and integration examples. + +--- + +## Table of Contents + +1. [Quick Start](#quick-start) +2. [Overview & Architecture](#overview--architecture) +3. [Authentication & Authorization](#authentication--authorization) +4. [Response Format Standard](#response-format-standard) +5. [Error Handling](#error-handling) +6. [Rate Limiting](#rate-limiting) +7. [Pagination](#pagination) +8. [Roles & Permissions](#roles--permissions) +9. [Tenant / Site / Sector Scoping](#tenant--site--sector-scoping) +10. [Complete Endpoint Reference](#complete-endpoint-reference) +11. [Integration Examples](#integration-examples) +12. [Testing & Debugging](#testing--debugging) +13. [Change Management](#change-management) + +--- + +## Quick Start + +### Interactive Documentation + +- **Swagger UI**: `https://api.igny8.com/api/docs/` +- **ReDoc**: `https://api.igny8.com/api/redoc/` +- **OpenAPI Schema**: `https://api.igny8.com/api/schema/` + +### Basic Example + +```python +import requests + +BASE_URL = "https://api.igny8.com/api/v1" + +# 1. Login +response = requests.post( + f"{BASE_URL}/auth/login/", + json={"email": "user@example.com", "password": "password"} +) +data = response.json() + +if data['success']: + token = data['data']['access'] + + # 2. Use token for authenticated requests + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + + # 3. Get keywords + response = requests.get( + f"{BASE_URL}/planner/keywords/", + headers=headers + ) + result = response.json() + + if result['success']: + keywords = result['results'] # Paginated results + print(f"Found {result['count']} keywords") +``` + +--- + +## Overview & Architecture + +### API Standard v1.0 - Key Principles + +1. **Unified Response Format**: All endpoints return consistent JSON structure +2. **Layered Authorization**: Authentication → Tenant Access → Role → Site/Sector +3. **Centralized Error Handling**: All errors wrapped in unified format +4. **Scoped Rate Limiting**: Different limits for different operation types +5. **Tenant Isolation**: All resources scoped by account/site/sector +6. **Request Tracking**: Every request has a unique ID for debugging + +### Base URL Structure + +``` +Production: https://api.igny8.com/api/v1/ +Development: http://localhost:8000/api/v1/ +``` + +### Module Namespaces + +``` +/api/v1/ +├── auth/ # Authentication and user management +├── planner/ # Keywords, clusters, content ideas +├── writer/ # Tasks, content, images +├── linker/ # Internal linking operations +├── optimizer/ # Content optimization and scoring +├── publisher/ # Publishing records and site deployment +├── site-builder/ # Site blueprint management +├── automation/ # Automation rules and scheduled tasks +├── integration/ # External platform integrations +├── system/ # Settings, prompts, integrations +└── billing/ # Credits, transactions, usage +``` + +### Technology Stack + +- **Framework**: Django REST Framework (DRF) +- **Authentication**: JWT Bearer tokens (primary), Session (fallback), Basic (fallback) +- **Pagination**: CustomPageNumberPagination (default: 10, max: 100) +- **Rate Limiting**: Scoped throttles per module/operation type +- **OpenAPI**: drf-spectacular for schema generation + +### Implementation Status + +✅ **100% Complete** - All endpoints implemented with: +- Unified response format +- Proper authentication and authorization +- Rate limiting configured +- Error handling standardized +- Request ID tracking +- Complete Swagger/OpenAPI documentation + +--- + +## Authentication & Authorization + +### Authentication Methods + +#### Primary: JWT Bearer Token + +``` +Authorization: Bearer +``` + +**Token Characteristics:** +- Contains `user_id` and `account_id` +- Type: `access` (15-minute expiry) +- Automatically sets `request.account` via middleware +- Resolves account → tenant context automatically + +**Token Payload:** +```json +{ + "user_id": 1, + "account_id": 1, + "type": "access", + "exp": 1234567890 +} +``` + +#### Fallback Methods + +1. **Session Authentication** (admin panel) + - Class: `CSRFExemptSessionAuthentication` + - Use case: Django admin panel (`/admin/`) + +2. **Basic Authentication** (debug/testing) + - Class: `rest_framework.authentication.BasicAuthentication` + - Use case: API testing tools (Postman, curl) + +### Authentication Order + +1. JWT Token Authentication (tried first) +2. Session Authentication (fallback) +3. Basic Authentication (last fallback) +4. If all fail: 401 Unauthorized + +### Getting an Access Token + +**Login Endpoint:** +```http +POST /api/v1/auth/login/ +Content-Type: application/json + +{ + "email": "user@example.com", + "password": "your_password" +} +``` + +**Response:** +```json +{ + "success": true, + "data": { + "user": { + "id": 1, + "email": "user@example.com", + "username": "user", + "role": "owner", + "account": { ... } + }, + "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + }, + "request_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +### Token Expiration + +- **Access Token**: 15 minutes +- **Refresh Token**: 7 days + +**Refresh Token:** +```http +POST /api/v1/auth/refresh/ +Content-Type: application/json + +{ + "refresh": "your_refresh_token" +} +``` + +### Public Endpoints (No Authentication Required) + +- `POST /api/v1/auth/register/` - User registration +- `POST /api/v1/auth/login/` - User login +- `GET /api/v1/auth/plans/` - List plans +- `GET /api/v1/auth/industries/` - List industries +- `GET /api/v1/system/status/` - System health check +- `GET /api/v1/system/ping/` - Health check endpoint +- `GET /api/v1/publisher/sites/{site_id}/definition/` - Get site definition for Sites Renderer + +**All other endpoints require JWT authentication.** + +### Authorization Layers + +Every endpoint enforces layered authorization: + +1. **User Authentication**: User must be authenticated +2. **Tenant Access**: User must belong to the tenant/account +3. **Role Authorization**: User must have appropriate role +4. **Site/Sector Access**: User must have access to requested site/sector + +--- + +## Response Format Standard + +### Mandatory Format + +**This is the global standard for all endpoints - no exceptions.** + +### Success Response + +```json +{ + "success": true, + "data": { + "id": 1, + "name": "Example Keyword", + "status": "active" + }, + "message": "Optional human-readable success message", + "request_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +### Paginated Response + +```json +{ + "success": true, + "count": 120, + "next": "http://api.igny8.com/api/v1/planner/keywords/?page=3", + "previous": "http://api.igny8.com/api/v1/planner/keywords/?page=1", + "results": [ + {"id": 1, "name": "Keyword 1"}, + {"id": 2, "name": "Keyword 2"}, + ... + ], + "request_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +### Error Response + +```json +{ + "success": false, + "error": "Readable top-level error message", + "errors": { + "field_name": ["Field-specific error messages"] + }, + "request_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +### Response Helper Functions + +**File:** `backend/igny8_core/api/response.py` + +```python +from igny8_core.api.response import success_response, error_response, paginated_response + +# Success response +return success_response( + data={"id": 1, "name": "Example"}, + message="Resource created successfully", + status_code=status.HTTP_201_CREATED +) + +# Error response +return error_response( + error="Validation failed", + errors={"email": ["Invalid email format"]}, + status_code=status.HTTP_400_BAD_REQUEST +) + +# Paginated response +paginator = CustomPageNumberPagination() +page = paginator.paginate_queryset(queryset, request) +serializer = MySerializer(page, many=True) +paginated_data = paginator.get_paginated_response(serializer.data).data +return paginated_response(paginated_data, message="Resources retrieved successfully") +``` + +--- + +## Error Handling + +### HTTP Status Codes + +| Code | Meaning | Description | +|------|---------|-------------| +| 200 | OK | Request successful | +| 201 | Created | Resource created successfully | +| 204 | No Content | Resource deleted successfully | +| 400 | Bad Request | Validation error or invalid request | +| 401 | Unauthorized | Authentication required | +| 403 | Forbidden | Permission denied | +| 404 | Not Found | Resource not found | +| 409 | Conflict | Resource conflict (e.g., duplicate) | +| 422 | Unprocessable Entity | Validation failed | +| 429 | Too Many Requests | Rate limit exceeded | +| 500 | Internal Server Error | Server error | + +### Centralized Exception Handler + +**File:** `backend/igny8_core/api/exception_handlers.py` + +All exceptions are handled by a centralized exception handler that: +- Wraps all errors in unified format +- Uses proper HTTP status codes +- Includes sanitized validation errors under `errors` +- Always attaches `request_id` for error tracking +- Logs full exception details +- In DEBUG mode: includes traceback + request context + +### Error Response Examples + +**Validation Error (400):** +```json +{ + "success": false, + "error": "Validation failed", + "errors": { + "email": ["Invalid email format"], + "password": ["Password must be at least 8 characters"] + }, + "request_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +**Authentication Error (401):** +```json +{ + "success": false, + "error": "Authentication required", + "request_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +**Permission Error (403):** +```json +{ + "success": false, + "error": "Permission denied", + "request_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +**Rate Limit (429):** +```json +{ + "success": false, + "error": "Rate limit exceeded", + "request_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +### Server-side Logging + +- All 4xx errors logged as **warning** +- All 5xx errors logged as **error** +- Structured format with timestamp, request_id, endpoint, user_id, account_id, status_code, error_message +- Rotating log files +- Sentry integration hooks for production + +--- + +## Rate Limiting + +### Rate Limiting Configuration + +Rate limits are scoped by operation type. Check response headers for limit information: + +- `X-Throttle-Limit`: Maximum requests allowed +- `X-Throttle-Remaining`: Remaining requests in current window +- `X-Throttle-Reset`: Time when limit resets (Unix timestamp) + +### Rate Limit Scopes + +| Scope | Limit | Description | +|-------|-------|-------------| +| `ai_function` | 10/min | AI content generation, clustering | +| `image_gen` | 15/min | Image generation | +| `content_write` | 30/min | Content creation, updates | +| `content_read` | 100/min | Content listing, retrieval | +| `auth` | 20/min | Login, register, password reset | +| `auth_strict` | 5/min | Sensitive auth operations | +| `planner` | 60/min | Keyword, cluster, idea operations | +| `planner_ai` | 10/min | AI-powered planner operations | +| `writer` | 60/min | Task, content management | +| `writer_ai` | 10/min | AI-powered writer operations | +| `system` | 100/min | Settings, prompts, profiles | +| `system_admin` | 30/min | Admin-only system operations | +| `billing` | 30/min | Credit queries, usage logs | +| `billing_admin` | 10/min | Credit management (admin) | +| `default` | 100/min | Default for endpoints without scope | + +### Handling Rate Limits + +When rate limited (429), the response includes: +- Error message: "Rate limit exceeded" +- Headers with reset time +- Wait until `X-Throttle-Reset` before retrying + +**Example:** +```http +HTTP/1.1 429 Too Many Requests +X-Throttle-Limit: 60 +X-Throttle-Remaining: 0 +X-Throttle-Reset: 1700123456 +Retry-After: 60 + +{ + "success": false, + "error": "Rate limit exceeded", + "request_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +### Debug Mode Bypass + +Set `IGNY8_DEBUG_THROTTLE=True` or `DEBUG=True` to bypass throttling in development. + +--- + +## Pagination + +### Pagination Configuration + +**Default Settings:** +- Default page size: **10** +- Maximum page size: **100** +- Query parameter: `page_size` (optional) +- Page parameter: `page` (default: 1) + +### Query Parameters + +**Pagination:** +``` +?page=2&page_size=25 +``` + +**Filtering:** +``` +?status=active +?site_id=1 +?sector_id=2 +?cluster_id=5 +``` + +**Search:** +``` +?search=keyword +``` + +**Ordering:** +``` +?ordering=-created_at +?ordering=name,status +``` + +### Pagination Response Format + +```json +{ + "success": true, + "count": 150, + "next": "http://api.igny8.com/api/v1/planner/keywords/?page=3&page_size=25", + "previous": "http://api.igny8.com/api/v1/planner/keywords/?page=1&page_size=25", + "results": [ + // Array of results + ], + "request_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +### Pagination Fields + +- `count`: Total number of items +- `next`: URL to next page (null if last page) +- `previous`: URL to previous page (null if first page) +- `results`: Array of items for current page + +--- + +## Roles & Permissions + +### Role Hierarchy + +``` +owner > admin > editor > viewer > system_bot +``` + +### Standard Permission Classes + +**File:** `backend/igny8_core/api/permissions.py` + +| Permission Class | Description | Use Case | +|-----------------|------------|----------| +| `IsAuthenticatedAndActive` | User authenticated and active | Base permission for most endpoints | +| `HasTenantAccess` | User belongs to tenant/account | Tenant isolation | +| `IsViewerOrAbove` | Viewer, editor, admin, or owner | Read-only operations | +| `IsEditorOrAbove` | Editor, admin, or owner | Content operations | +| `IsAdminOrOwner` | Admin or owner only | Settings, keys, billing | + +### Permission Matrix by Endpoint Type + +| Endpoint Type | Required Permissions | Roles Allowed | +|--------------|---------------------|---------------| +| Public (register, login) | `AllowAny` | Anyone | +| Read-only (list, retrieve) | `IsAuthenticatedAndActive` + `HasTenantAccess` | All authenticated users | +| Content operations | `IsAuthenticatedAndActive` + `HasTenantAccess` + `IsEditorOrAbove` | Editor, Admin, Owner | +| User management | `IsAuthenticatedAndActive` + `HasTenantAccess` + `IsAdminOrOwner` | Admin, Owner | +| Billing/Transactions | `IsAuthenticatedAndActive` + `HasTenantAccess` + `IsAdminOrOwner` | Admin, Owner | +| Integration settings | `IsAuthenticatedAndActive` + `HasTenantAccess` + `IsAdminOrOwner` | Admin, Owner | + +--- + +## Tenant / Site / Sector Scoping + +### Scoping Rules + +Every resource created or fetched must be scoped by: + +1. **Account/Tenant** - User's account +2. **Site** - Specific site within account +3. **Sector** - Specific sector within site + +### Enforcement + +**Base Classes:** +- `AccountModelViewSet` - Handles account isolation +- `SiteSectorModelViewSet` - Filters queries by site/sector + +**Requirements:** +- All custom actions must use `.get_queryset()` to avoid bypassing filters +- Any ID list must be verified to belong to the authenticated tenant +- Site/sector access validated based on user role + +### Scoping Example + +```python +class KeywordViewSet(SiteSectorModelViewSet): + # Automatically filters by: + # 1. account (from request.account) + # 2. site_id (from query params or request) + # 3. sector_id (from query params or request) + + queryset = Keyword.objects.all() + serializer_class = KeywordSerializer + + def get_queryset(self): + # Base class handles account/site/sector filtering + queryset = super().get_queryset() + # Additional filtering can be added here + return queryset +``` + +--- + +## Complete Endpoint Reference + +### Authentication Endpoints + +**Base Path**: `/api/v1/auth/` + +#### POST `/api/v1/auth/register/` +**Purpose**: User registration +**Authentication**: None (AllowAny) + +**Request:** +```json +{ + "email": "user@example.com", + "password": "password123", + "password_confirm": "password123" +} +``` + +**Response:** +```json +{ + "success": true, + "data": { + "user": { + "id": 1, + "email": "user@example.com", + "role": "owner", + "account": { ... } + } + }, + "message": "Registration successful" +} +``` + +#### POST `/api/v1/auth/login/` +**Purpose**: User login +**Authentication**: None (AllowAny) + +**Request:** +```json +{ + "email": "user@example.com", + "password": "password123" +} +``` + +**Response:** +```json +{ + "success": true, + "data": { + "user": { ... }, + "access": "eyJ0eXAiOiJKV1QiLCJhbGc...", + "refresh": "eyJ0eXAiOiJKV1QiLCJhbGc...", + "access_expires_at": "2025-01-XXT...", + "refresh_expires_at": "2025-01-XXT..." + }, + "message": "Login successful", + "request_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +#### POST `/api/v1/auth/refresh/` +**Purpose**: Refresh access token +**Authentication**: None (requires refresh token) + +**Request:** +```json +{ + "refresh": "your_refresh_token" +} +``` + +#### POST `/api/v1/auth/change-password/` +**Purpose**: Change user password +**Authentication**: Required (IsAuthenticated) + +**Request:** +```json +{ + "old_password": "oldpass123", + "new_password": "newpass123", + "new_password_confirm": "newpass123" +} +``` + +#### GET `/api/v1/auth/me/` +**Purpose**: Get current user information +**Authentication**: Required (IsAuthenticated) + +**Response:** +```json +{ + "success": true, + "data": { + "user": { + "id": 1, + "email": "user@example.com", + "role": "owner", + "account": { ... }, + "accessible_sites": [ ... ] + } + } +} +``` + +### User Management Endpoints + +**Base Path**: `/api/v1/auth/users/` +**Permission**: IsOwnerOrAdmin + +**Standard CRUD:** +- `GET /api/v1/auth/users/` - List users +- `POST /api/v1/auth/users/` - Create user +- `GET /api/v1/auth/users/{id}/` - Get user +- `PUT /api/v1/auth/users/{id}/` - Update user +- `DELETE /api/v1/auth/users/{id}/` - Delete user + +**Custom Actions:** +- `POST /api/v1/auth/users/invite/` - Invite user +- `POST /api/v1/auth/users/{id}/activate/` - Activate user + +### Account Management Endpoints + +**Base Path**: `/api/v1/auth/accounts/` +**Permission**: IsOwnerOrAdmin + +**Standard CRUD:** +- `GET /api/v1/auth/accounts/` - List accounts +- `POST /api/v1/auth/accounts/` - Create account +- `GET /api/v1/auth/accounts/{id}/` - Get account +- `PUT /api/v1/auth/accounts/{id}/` - Update account +- `DELETE /api/v1/auth/accounts/{id}/` - Delete account + +### Site Management Endpoints + +**Base Path**: `/api/v1/auth/sites/` +**Permission**: IsEditorOrAbove + +**Standard CRUD:** +- `GET /api/v1/auth/sites/` - List sites +- `POST /api/v1/auth/sites/` - Create site +- `GET /api/v1/auth/sites/{id}/` - Get site +- `PUT /api/v1/auth/sites/{id}/` - Update site +- `DELETE /api/v1/auth/sites/{id}/` - Delete site + +**Custom Actions:** +- `GET /api/v1/auth/sites/{id}/sectors/` - Get site sectors +- `POST /api/v1/auth/sites/{id}/set_active/` - Set active site +- `POST /api/v1/auth/sites/{id}/select_sectors/` - Select sectors + +### Sector Management Endpoints + +**Base Path**: `/api/v1/auth/sectors/` +**Permission**: IsEditorOrAbove + +**Standard CRUD:** +- `GET /api/v1/auth/sectors/` - List sectors +- `POST /api/v1/auth/sectors/` - Create sector +- `GET /api/v1/auth/sectors/{id}/` - Get sector +- `PUT /api/v1/auth/sectors/{id}/` - Update sector +- `DELETE /api/v1/auth/sectors/{id}/` - Delete sector + +### Planner Module Endpoints + +**Base Path**: `/api/v1/planner/` + +#### Keyword Management + +**Base Path**: `/api/v1/planner/keywords/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess +**Inherits**: SiteSectorModelViewSet + +**Standard CRUD:** +- `GET /api/v1/planner/keywords/` - List keywords (paginated) +- `POST /api/v1/planner/keywords/` - Create keyword +- `GET /api/v1/planner/keywords/{id}/` - Get keyword +- `PUT /api/v1/planner/keywords/{id}/` - Update keyword +- `DELETE /api/v1/planner/keywords/{id}/` - Delete keyword + +**Filtering:** +- `status` - Filter by status +- `cluster_id` - Filter by cluster +- `seed_keyword__intent` - Filter by intent +- `seed_keyword_id` - Filter by seed keyword ID +- `difficulty_min`, `difficulty_max` - Difficulty range +- `volume_min`, `volume_max` - Volume range +- `site_id` - Filter by site (query param) +- `sector_id` - Filter by sector (query param) + +**Search:** +- `search` - Search by keyword text + +**Ordering:** +- `ordering` - Order by `created_at`, `seed_keyword__volume`, `seed_keyword__difficulty` +- Default: `-created_at` (newest first) + +**Custom Actions:** +- `POST /api/v1/planner/keywords/bulk_delete/` - Bulk delete keywords + - Request: `{ "ids": [1, 2, 3] }` +- `POST /api/v1/planner/keywords/bulk_update_status/` - Bulk update status + - Request: `{ "ids": [1, 2, 3], "status": "active" }` +- `POST /api/v1/planner/keywords/bulk_add_from_seed/` - Add keywords from seed library + - Request: `{ "seed_keyword_ids": [1, 2, 3], "site_id": 1, "sector_id": 1 }` +- `GET /api/v1/planner/keywords/export/` - Export keywords to CSV +- `POST /api/v1/planner/keywords/import_keywords/` - Import keywords from CSV +- `POST /api/v1/planner/keywords/auto_cluster/` - Auto-cluster keywords using AI + - Request: `{ "ids": [1, 2, 3, ...], "sector_id": 1 }` + - Max Keywords: 20 per batch + - Returns: Celery task ID for progress tracking + +#### Cluster Management + +**Base Path**: `/api/v1/planner/clusters/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess +**Inherits**: SiteSectorModelViewSet + +**Standard CRUD:** +- `GET /api/v1/planner/clusters/` - List clusters +- `POST /api/v1/planner/clusters/` - Create cluster +- `GET /api/v1/planner/clusters/{id}/` - Get cluster +- `PUT /api/v1/planner/clusters/{id}/` - Update cluster +- `DELETE /api/v1/planner/clusters/{id}/` - Delete cluster + +**Filtering:** +- `status` - Filter by status +- `site_id` - Filter by site (query param) +- `sector_id` - Filter by sector (query param) + +**Search:** +- `search` - Search by cluster name + +**Custom Actions:** +- `POST /api/v1/planner/clusters/bulk_delete/` - Bulk delete clusters +- `POST /api/v1/planner/clusters/auto_generate_ideas/` - Auto-generate content ideas + - Request: `{ "ids": [1] }` (max 1 cluster per batch) + - Returns: Celery task ID for progress tracking + +#### Content Ideas Management + +**Base Path**: `/api/v1/planner/ideas/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess +**Inherits**: SiteSectorModelViewSet + +**Standard CRUD:** +- `GET /api/v1/planner/ideas/` - List content ideas +- `POST /api/v1/planner/ideas/` - Create content idea +- `GET /api/v1/planner/ideas/{id}/` - Get content idea +- `PUT /api/v1/planner/ideas/{id}/` - Update content idea +- `DELETE /api/v1/planner/ideas/{id}/` - Delete content idea + +**Filtering:** +- `status` - Filter by status +- `cluster_id` - Filter by cluster +- `content_type` - Filter by content type +- `site_id` - Filter by site (query param) +- `sector_id` - Filter by sector (query param) + +**Custom Actions:** +- `POST /api/v1/planner/ideas/bulk_delete/` - Bulk delete ideas +- `POST /api/v1/planner/ideas/bulk_queue_to_writer/` - Queue ideas to writer (create tasks) + - Request: `{ "ids": [1, 2, 3] }` + +### Writer Module Endpoints + +**Base Path**: `/api/v1/writer/` + +#### Task Management + +**Base Path**: `/api/v1/writer/tasks/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess +**Inherits**: SiteSectorModelViewSet + +**Standard CRUD:** +- `GET /api/v1/writer/tasks/` - List tasks +- `POST /api/v1/writer/tasks/` - Create task +- `GET /api/v1/writer/tasks/{id}/` - Get task +- `PUT /api/v1/writer/tasks/{id}/` - Update task +- `DELETE /api/v1/writer/tasks/{id}/` - Delete task + +**Filtering:** +- `status` - Filter by status (draft, in_progress, review, completed, archived) +- `cluster_id` - Filter by cluster +- `content_type` - Filter by content type +- `content_structure` - Filter by content structure +- `site_id` - Filter by site (query param) +- `sector_id` - Filter by sector (query param) + +**Search:** +- `search` - Search by title or keywords + +**Custom Actions:** +- `POST /api/v1/writer/tasks/bulk_delete/` - Bulk delete tasks +- `POST /api/v1/writer/tasks/bulk_update/` - Bulk update task status +- `POST /api/v1/writer/tasks/auto_generate_content/` - Auto-generate content using AI + - Request: `{ "ids": [1, 2, 3, ...] }` (max 50 tasks per batch) + - Returns: Celery task ID for progress tracking + +#### Content Management + +**Base Path**: `/api/v1/writer/content/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess +**Inherits**: SiteSectorModelViewSet + +**Standard CRUD:** +- `GET /api/v1/writer/content/` - List content +- `POST /api/v1/writer/content/` - Create content +- `GET /api/v1/writer/content/{id}/` - Get content +- `PUT /api/v1/writer/content/{id}/` - Update content +- `DELETE /api/v1/writer/content/{id}/` - Delete content + +**Filtering:** +- `status` - Filter by status +- `content_type` - Filter by content type +- `site_id` - Filter by site (query param) +- `sector_id` - Filter by sector (query param) + +**Custom Actions:** +- `POST /api/v1/writer/content/generate_image_prompts/` - Generate image prompts from content + - Request: `{ "ids": [1, 2, 3] }` + - Returns: Celery task ID for progress tracking + +#### Image Management + +**Base Path**: `/api/v1/writer/images/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess +**Inherits**: SiteSectorModelViewSet + +**Standard CRUD:** +- `GET /api/v1/writer/images/` - List images +- `POST /api/v1/writer/images/` - Create image +- `GET /api/v1/writer/images/{id}/` - Get image +- `PUT /api/v1/writer/images/{id}/` - Update image +- `DELETE /api/v1/writer/images/{id}/` - Delete image + +**Filtering:** +- `image_type` - Filter by type (featured, in_article, desktop, mobile) +- `status` - Filter by status +- `content_id` - Filter by content +- `task_id` - Filter by task +- `site_id` - Filter by site (query param) +- `sector_id` - Filter by sector (query param) + +**Custom Actions:** +- `GET /api/v1/writer/images/{id}/file/` - Get image file URL +- `GET /api/v1/writer/images/content_images/` - Get images for content + - Query Params: `content_id` (required) +- `POST /api/v1/writer/images/generate_images/` - Generate images using AI + - Request: `{ "ids": [1, 2, 3, ...] }` + - Returns: Celery task ID for progress tracking +- `POST /api/v1/writer/images/bulk_update/` - Bulk update image status + +### System Module Endpoints + +**Base Path**: `/api/v1/system/` + +#### AI Prompt Management + +**Base Path**: `/api/v1/system/prompts/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess +**Inherits**: AccountModelViewSet + +**Standard CRUD:** +- `GET /api/v1/system/prompts/` - List prompts +- `POST /api/v1/system/prompts/` - Create prompt +- `GET /api/v1/system/prompts/{id}/` - Get prompt +- `PUT /api/v1/system/prompts/{id}/` - Update prompt +- `DELETE /api/v1/system/prompts/{id}/` - Delete prompt + +**Custom Actions:** +- `GET /api/v1/system/prompts/by_type/{prompt_type}/` - Get prompt by type +- `POST /api/v1/system/prompts/save/` - Save prompt (requires editor/admin) +- `POST /api/v1/system/prompts/reset/` - Reset prompt to default + +#### Integration Settings + +**Base Path**: `/api/v1/system/settings/integrations/` +**Permission**: IsAdminOrOwner + +**Custom URL Patterns:** +- `GET /api/v1/system/settings/integrations/{pk}/` - Get integration settings +- `POST /api/v1/system/settings/integrations/{pk}/save/` - Save integration settings +- `PUT /api/v1/system/settings/integrations/{pk}/` - Update integration settings +- `POST /api/v1/system/settings/integrations/{pk}/test/` - Test connection + - Request: `{ "provider": "openai" }` or `{ "provider": "runware" }` +- `POST /api/v1/system/settings/integrations/{pk}/generate/` - Test image generation +- `GET /api/v1/system/settings/task_progress/{task_id}/` - Get Celery task progress +- `GET /api/v1/system/integrations/image_generation/` - Get image generation settings + +#### System Status + +**Base Path**: `/api/v1/system/` + +- `GET /api/v1/system/status/` - System health check (AllowAny) +- `GET /api/v1/system/ping/` - Health check endpoint (AllowAny) +- `GET /api/v1/system/request-metrics/{request_id}/` - Get request metrics for debugging + +### Linker Module Endpoints + +**Base Path**: `/api/v1/linker/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess + +#### Process Content for Internal Linking + +- `POST /api/v1/linker/process/` - Process single content item for internal linking + +**Request:** +```json +{ + "content_id": 123 +} +``` + +**Response:** +```json +{ + "success": true, + "data": { + "content_id": 123, + "suggested_links": [ + { + "target_content_id": 456, + "target_title": "Related Article", + "anchor_text": "relevant keyword", + "confidence": 0.85 + } + ] + } +} +``` + +#### Batch Process Content + +- `POST /api/v1/linker/batch_process/` - Process multiple content items for linking + +**Request:** +```json +{ + "content_ids": [123, 456, 789] +} +``` + +### Optimizer Module Endpoints + +**Base Path**: `/api/v1/optimizer/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess + +#### Optimize Content + +- `POST /api/v1/optimizer/optimize/` - Optimize content (auto-detects entry point) + +**Request:** +```json +{ + "ids": [123, 456], + "entry_point": "seo" // Optional: "seo", "readability", "engagement" +} +``` + +**Response:** +```json +{ + "success": true, + "data": { + "optimized_content": "...", + "seo_score": 85, + "readability_score": 78, + "engagement_score": 82 + } +} +``` + +#### Batch Optimize Content + +- `POST /api/v1/optimizer/batch_optimize/` - Batch optimize multiple content items + +**Request:** +```json +{ + "ids": [123, 456, 789], + "entry_point": "seo" // Optional +} +``` + +#### Analyze Content + +- `POST /api/v1/optimizer/analyze/` - Analyze content without optimization + +**Request:** +```json +{ + "id": 123 +} +``` + +**Response:** +```json +{ + "success": true, + "data": { + "seo_score": 75, + "readability_score": 70, + "engagement_score": 68, + "recommendations": [ + "Add more internal links", + "Improve heading structure" + ] + } +} +``` + +### Publisher Module Endpoints + +**Base Path**: `/api/v1/publisher/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess + +#### Publishing Records + +**Base Path**: `/api/v1/publisher/publishing-records/` +**Inherits**: SiteSectorModelViewSet + +**Standard CRUD:** +- `GET /api/v1/publisher/publishing-records/` - List publishing records (paginated) +- `POST /api/v1/publisher/publishing-records/` - Create publishing record +- `GET /api/v1/publisher/publishing-records/{id}/` - Get publishing record +- `PUT /api/v1/publisher/publishing-records/{id}/` - Update publishing record +- `DELETE /api/v1/publisher/publishing-records/{id}/` - Delete publishing record + +**Filtering:** +- `status` - Filter by publishing status +- `destination_type` - Filter by destination (wordpress, sites_renderer, etc.) +- `site_id` - Filter by site +- `sector_id` - Filter by sector + +#### Deployment Records + +**Base Path**: `/api/v1/publisher/deployments/` +**Inherits**: SiteSectorModelViewSet + +**Standard CRUD:** +- `GET /api/v1/publisher/deployments/` - List deployment records (paginated) +- `POST /api/v1/publisher/deployments/` - Create deployment record +- `GET /api/v1/publisher/deployments/{id}/` - Get deployment record +- `PUT /api/v1/publisher/deployments/{id}/` - Update deployment record +- `DELETE /api/v1/publisher/deployments/{id}/` - Delete deployment record + +#### Publishing Actions + +- `POST /api/v1/publisher/publish/` - Publish content to destination + +**Request:** +```json +{ + "content_id": 123, + "destination_type": "wordpress", + "destination_id": 1 +} +``` + +- `POST /api/v1/publisher/deploy/` - Deploy site blueprint + +**Request:** +```json +{ + "blueprint_id": 456, + "environment": "production" +} +``` + +- `POST /api/v1/publisher/check_readiness/` - Check deployment readiness + +**Request:** +```json +{ + "blueprint_id": 456 +} +``` + +#### Public Endpoint (No Authentication) + +- `GET /api/v1/publisher/sites/{site_id}/definition/` - Get site definition for Sites Renderer + +**Response:** +```json +{ + "success": true, + "data": { + "site_id": 1, + "slug": "example-site", + "blueprints": [...], + "pages": [...] + } +} +``` + +### Automation Module Endpoints + +**Base Path**: `/api/v1/automation/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess + +#### Automation Rules + +**Base Path**: `/api/v1/automation/rules/` +**Inherits**: SiteSectorModelViewSet + +**Standard CRUD:** +- `GET /api/v1/automation/rules/` - List automation rules (paginated) +- `POST /api/v1/automation/rules/` - Create automation rule +- `GET /api/v1/automation/rules/{id}/` - Get rule details +- `PUT /api/v1/automation/rules/{id}/` - Update rule +- `DELETE /api/v1/automation/rules/{id}/` - Delete rule + +**Custom Actions:** +- `POST /api/v1/automation/rules/{id}/execute/` - Manually execute automation rule + +**Filtering:** +- `status` - Filter by rule status (active, inactive) +- `trigger_type` - Filter by trigger type +- `site_id` - Filter by site +- `sector_id` - Filter by sector + +#### Scheduled Tasks + +**Base Path**: `/api/v1/automation/scheduled-tasks/` +**Inherits**: AccountModelViewSet + +**Standard CRUD:** +- `GET /api/v1/automation/scheduled-tasks/` - List scheduled tasks (paginated) +- `POST /api/v1/automation/scheduled-tasks/` - Create scheduled task +- `GET /api/v1/automation/scheduled-tasks/{id}/` - Get task details +- `PUT /api/v1/automation/scheduled-tasks/{id}/` - Update task +- `DELETE /api/v1/automation/scheduled-tasks/{id}/` - Delete task + +**Filtering:** +- `status` - Filter by task status +- `task_type` - Filter by task type +- `next_run_date` - Filter by next run date + +### Integration Module Endpoints + +**Base Path**: `/api/v1/integration/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess + +#### Site Integrations + +**Base Path**: `/api/v1/integration/integrations/` +**Inherits**: SiteSectorModelViewSet + +**Standard CRUD:** +- `GET /api/v1/integration/integrations/` - List site integrations (paginated) +- `POST /api/v1/integration/integrations/` - Create integration +- `GET /api/v1/integration/integrations/{id}/` - Get integration details +- `PUT /api/v1/integration/integrations/{id}/` - Update integration +- `DELETE /api/v1/integration/integrations/{id}/` - Delete integration + +**Custom Actions:** +- `POST /api/v1/integration/integrations/{id}/test_connection/` - Test connection to integrated platform + +**Request:** +```json +{} +``` + +**Response:** +```json +{ + "success": true, + "data": { + "connected": true, + "message": "Connection successful" + } +} +``` + +- `POST /api/v1/integration/integrations/{id}/sync/` - Sync content with integrated platform + +**Request:** +```json +{ + "content_ids": [123, 456], + "sync_type": "full" // or "incremental" +} +``` + +- `GET /api/v1/integration/integrations/{id}/sync_health/` - Get sync health status + +**Response:** +```json +{ + "success": true, + "data": { + "last_sync": "2025-01-XXT...", + "sync_status": "healthy", + "pending_items": 0, + "failed_items": 0 + } +} +``` + +**Filtering:** +- `platform_type` - Filter by platform (wordpress, shopify, etc.) +- `status` - Filter by integration status +- `site_id` - Filter by site +- `sector_id` - Filter by sector + +### Billing Module Endpoints + +**Base Path**: `/api/v1/billing/` + +#### Credit Balance + +**Base Path**: `/api/v1/billing/credits/balance/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess + +- `GET /api/v1/billing/credits/balance/balance/` - Get credit balance + +**Response:** +```json +{ + "success": true, + "data": { + "credits": 1000, + "plan_credits_per_month": 500, + "credits_used_this_month": 250, + "credits_remaining": 750 + } +} +``` + +#### Credit Usage + +**Base Path**: `/api/v1/billing/credits/usage/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess + +**Standard CRUD:** +- `GET /api/v1/billing/credits/usage/` - List usage logs (paginated) +- `GET /api/v1/billing/credits/usage/{id}/` - Get usage log + +**Filtering:** +- `operation_type` - Filter by operation type +- `start_date` - Filter by start date (YYYY-MM-DD) +- `end_date` - Filter by end date (YYYY-MM-DD) + +**Custom Actions:** +- `GET /api/v1/billing/credits/usage/summary/` - Get usage summary +- `GET /api/v1/billing/credits/usage/limits/` - Get usage limits + +#### Credit Transactions + +**Base Path**: `/api/v1/billing/credits/transactions/` +**Permission**: IsAdminOrOwner + +**Standard CRUD:** +- `GET /api/v1/billing/credits/transactions/` - List transactions (paginated) +- `GET /api/v1/billing/credits/transactions/{id}/` - Get transaction + +**Filtering:** +- `transaction_type` - Filter by type +- `start_date` - Filter by start date +- `end_date` - Filter by end date + +--- + +## Integration Examples + +### Python + +```python +import requests + +BASE_URL = "https://api.igny8.com/api/v1" + +# Login +response = requests.post( + f"{BASE_URL}/auth/login/", + json={"email": "user@example.com", "password": "password"} +) +data = response.json() + +if data['success']: + token = data['data']['access'] + + # Use token for authenticated requests + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + + # Get keywords + response = requests.get( + f"{BASE_URL}/planner/keywords/?page=1&page_size=25", + headers=headers + ) + result = response.json() + + if result['success']: + keywords = result['results'] + print(f"Found {result['count']} keywords") + else: + print(f"Error: {result['error']}") +``` + +### JavaScript + +```javascript +const BASE_URL = 'https://api.igny8.com/api/v1'; + +// Login +const loginResponse = await fetch(`${BASE_URL}/auth/login/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email: 'user@example.com', + password: 'password' + }) +}); + +const loginData = await loginResponse.json(); + +if (loginData.success) { + const token = loginData.data.access; + + // Use token for authenticated requests + const headers = { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }; + + // Get keywords + const keywordsResponse = await fetch( + `${BASE_URL}/planner/keywords/?page=1&page_size=25`, + { headers } + ); + + const keywordsData = await keywordsResponse.json(); + + if (keywordsData.success) { + const keywords = keywordsData.results; + console.log(`Found ${keywordsData.count} keywords`); + } else { + console.error('Error:', keywordsData.error); + } +} +``` + +### cURL + +```bash +# Login +curl -X POST https://api.igny8.com/api/v1/auth/login/ \ + -H "Content-Type: application/json" \ + -d '{"email":"user@example.com","password":"password"}' + +# Get keywords (with token) +curl -X GET https://api.igny8.com/api/v1/planner/keywords/ \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "Content-Type: application/json" +``` + +### Frontend Integration (TypeScript) + +```typescript +// Using fetchAPI wrapper (automatically extracts data from unified format) +import { fetchAPI } from './services/api'; + +// List keywords +const response = await fetchAPI('/v1/planner/keywords/?page=1&page_size=25'); +// response is already extracted: { count, next, previous, results } + +// Create keyword +const newKeyword = await fetchAPI('/v1/planner/keywords/', { + method: 'POST', + body: JSON.stringify({ + keyword: 'example keyword', + site_id: 1, + sector_id: 2, + status: 'active' + }) +}); + +// Error handling +try { + const data = await fetchAPI('/v1/planner/keywords/'); +} catch (error) { + // Error is already parsed from unified format + console.error(error.message); + if (error.errors) { + // Handle field-specific errors + Object.keys(error.errors).forEach(field => { + console.error(`${field}: ${error.errors[field].join(', ')}`); + }); + } +} +``` + +### WordPress Plugin Integration (PHP) + +```php +class Igny8API { + private $base_url = 'https://api.igny8.com/api/v1'; + private $token; + + public function login($email, $password) { + $response = wp_remote_post($this->base_url . '/auth/login/', [ + 'headers' => ['Content-Type' => 'application/json'], + 'body' => json_encode(['email' => $email, 'password' => $password]) + ]); + + $data = json_decode(wp_remote_retrieve_body($response), true); + + if ($data['success']) { + $this->token = $data['data']['access']; + return true; + } + + return false; + } + + public function getKeywords($page = 1, $page_size = 25) { + $response = wp_remote_get( + $this->base_url . '/planner/keywords/?page=' . $page . '&page_size=' . $page_size, + [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $this->token, + 'Content-Type' => 'application/json' + ] + ] + ); + + $data = json_decode(wp_remote_retrieve_body($response), true); + + if ($data['success']) { + return $data['results']; + } + + return []; + } +} +``` + +--- + +## Testing & Debugging + +### Request ID Tracking + +Every API request includes a unique `request_id` in the response. Use this ID for: +- Debugging issues +- Log correlation +- Support requests + +The `request_id` is included in: +- All success responses +- All error responses +- Response headers (`X-Request-ID`) + +### Progress Tracking (AI Functions) + +All AI functions return a Celery task ID for progress tracking: + +**Request:** +```json +POST /api/v1/planner/keywords/auto_cluster/ +{ + "ids": [1, 2, 3, 4, 5], + "sector_id": 1 +} +``` + +**Response:** +```json +{ + "success": true, + "data": { + "task_id": "abc123-def456-ghi789" + } +} +``` + +**Poll Progress:** +```json +GET /api/v1/system/settings/task_progress/abc123-def456-ghi789/ +``` + +**Progress Response:** +```json +{ + "state": "PROGRESS", + "meta": { + "phase": "AI_CALL", + "percentage": 50, + "message": "Processing keywords...", + "request_steps": [...], + "response_steps": [...], + "cost": 0.05, + "tokens": 1500 + } +} +``` + +### Error Handling Best Practices + +**401 Unauthorized**: Trigger logout → redirect to login +**403 Forbidden**: Show permission alert +**429 Too Many Requests**: Show rate limit warning with retry time +**4xx/5xx**: Display error message from `error` field + +### Rate Limit Monitoring + +```typescript +const throttleLimit = response.headers.get('X-Throttle-Limit'); +const throttleRemaining = response.headers.get('X-Throttle-Remaining'); + +if (parseInt(throttleRemaining) < 5) { + showNotification('Approaching rate limit', 'warning'); +} +``` + +--- + +## Change Management + +### Versioning Strategy + +- **v1** remains stable long-term +- Breaking changes require **v2** +- Deprecations allowed only with explicit timeline +- Non-breaking changes can be added to v1 + +### Breaking Changes + +**Definition:** Changes that require client code updates + +**Examples:** +- Removing an endpoint +- Changing response structure +- Changing authentication method +- Removing a field from response + +**Process:** +1. Document breaking change +2. Provide migration guide +3. Deprecate in v1 with timeline +4. Implement in v2 +5. Maintain v1 for deprecation period + +### Non-Breaking Changes + +**Definition:** Changes that don't require client code updates + +**Examples:** +- Adding new endpoints +- Adding optional fields to response +- Adding new query parameters +- Performance improvements + +### Changelog + +All API changes are documented in `CHANGELOG.md` with: +- Version number +- Date +- Type (Added, Changed, Fixed, Deprecated, Removed, Security) +- Description +- Affected areas +- Migration notes (if applicable) + +--- + +## Summary + +### Implementation Status + +✅ **100% Complete** - All endpoints implemented with: +- Unified response format (`{success, data, message, errors, request_id}`) +- Proper authentication and authorization (JWT Bearer tokens) +- Rate limiting configured (scoped by operation type) +- Error handling standardized (centralized exception handler) +- Request ID tracking (every request has unique ID) +- Complete Swagger/OpenAPI documentation +- Tenant/site/sector scoping (automatic filtering) +- Pagination standardized (default: 10, max: 100) + +### Key Features + +- **100+ endpoints** across 5 modules +- **Unified response format** for all endpoints +- **Scoped rate limiting** (10-100 requests/minute depending on operation) +- **Layered authorization** (Authentication → Tenant → Role → Site/Sector) +- **Complete OpenAPI documentation** (Swagger UI, ReDoc) +- **Request ID tracking** for debugging +- **Progress tracking** for AI functions +- **Comprehensive error handling** with clear messages + +### Access Points + +- **Interactive Documentation**: `https://api.igny8.com/api/docs/` +- **ReDoc**: `https://api.igny8.com/api/redoc/` +- **OpenAPI Schema**: `https://api.igny8.com/api/schema/` + +--- + +**Last Updated**: 2025-01-XX (Added 6 missing modules: Linker, Optimizer, Publisher, Site Builder, Automation, Integration) +**API Version**: 1.0.0 +**Status**: ✅ **100% IMPLEMENTED** - All 10 modules documented with complete endpoint reference + diff --git a/old-docs/API/API-COMPLETE-REFERENCE-LATEST.md b/old-docs/API/API-COMPLETE-REFERENCE-LATEST.md new file mode 100644 index 00000000..814c263c --- /dev/null +++ b/old-docs/API/API-COMPLETE-REFERENCE-LATEST.md @@ -0,0 +1,1516 @@ +# IGNY8 API Complete Reference v1.0 + +**Base URL**: `https://api.igny8.com/api/v1/` +**Version**: 1.0.0 +**Last Updated**: 2025-01-XX +**Status**: ✅ **100% IMPLEMENTED** - All endpoints use unified format + +**Purpose**: Complete, unified reference for IGNY8 API covering authentication, endpoints, response formats, error handling, rate limiting, permissions, and integration examples. + +--- + +## Table of Contents + +1. [Quick Start](#quick-start) +2. [Overview & Architecture](#overview--architecture) +3. [Authentication & Authorization](#authentication--authorization) +4. [Response Format Standard](#response-format-standard) +5. [Error Handling](#error-handling) +6. [Rate Limiting](#rate-limiting) +7. [Pagination](#pagination) +8. [Roles & Permissions](#roles--permissions) +9. [Tenant / Site / Sector Scoping](#tenant--site--sector-scoping) +10. [Complete Endpoint Reference](#complete-endpoint-reference) +11. [Integration Examples](#integration-examples) +12. [Testing & Debugging](#testing--debugging) +13. [Change Management](#change-management) + +--- + +## Quick Start + +### Interactive Documentation + +- **Swagger UI**: `https://api.igny8.com/api/docs/` +- **ReDoc**: `https://api.igny8.com/api/redoc/` +- **OpenAPI Schema**: `https://api.igny8.com/api/schema/` + +### Basic Example + +```python +import requests + +BASE_URL = "https://api.igny8.com/api/v1" + +# 1. Login +response = requests.post( + f"{BASE_URL}/auth/login/", + json={"email": "user@example.com", "password": "password"} +) +data = response.json() + +if data['success']: + token = data['data']['access'] + + # 2. Use token for authenticated requests + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + + # 3. Get keywords + response = requests.get( + f"{BASE_URL}/planner/keywords/", + headers=headers + ) + result = response.json() + + if result['success']: + keywords = result['results'] # Paginated results + print(f"Found {result['count']} keywords") +``` + +--- + +## Overview & Architecture + +### API Standard v1.0 - Key Principles + +1. **Unified Response Format**: All endpoints return consistent JSON structure +2. **Layered Authorization**: Authentication → Tenant Access → Role → Site/Sector +3. **Centralized Error Handling**: All errors wrapped in unified format +4. **Scoped Rate Limiting**: Different limits for different operation types +5. **Tenant Isolation**: All resources scoped by account/site/sector +6. **Request Tracking**: Every request has a unique ID for debugging + +### Base URL Structure + +``` +Production: https://api.igny8.com/api/v1/ +Development: http://localhost:8000/api/v1/ +``` + +### Module Namespaces + +``` +/api/v1/ +├── auth/ # Authentication and user management +├── account/ # Account settings, team, and usage analytics +├── integration/ # Site integrations and sync +├── system/ # Settings, prompts, integrations +├── admin/billing/ # Admin-only billing management +├── billing/ # Credits, transactions, usage +├── planner/ # Keywords, clusters, content ideas +├── writer/ # Tasks, content, images +├── automation/ # Automation configuration and runs +├── linker/ # Internal linking operations +├── optimizer/ # Content optimization operations +└── publisher/ # Publishing records and deployments +``` + +### Module → Tag Map (Swagger/ReDoc) +- Authentication → `Authentication` +- Account → `Account` +- Integration → `Integration` +- System → `System` +- Admin Billing → `Admin Billing` +- Billing → `Billing` +- Planner → `Planner` +- Writer → `Writer` +- Automation → `Automation` +- Linker → `Linker` +- Optimizer → `Optimizer` +- Publisher → `Publisher` + +Tag display order (docs): Authentication, Account, Integration, System, Admin Billing, Billing, Planner, Writer, Automation, Linker, Optimizer, Publisher + +### Technology Stack + +- **Framework**: Django REST Framework (DRF) +- **Authentication**: JWT Bearer tokens (primary), Session (fallback), Basic (fallback) +- **Pagination**: CustomPageNumberPagination (default: 10, max: 100) +- **Rate Limiting**: Scoped throttles per module/operation type +- **OpenAPI**: drf-spectacular for schema generation + +### Implementation Status + +✅ **100% Complete** - All endpoints implemented with: +- Unified response format +- Proper authentication and authorization +- Rate limiting configured +- Error handling standardized +- Request ID tracking +- Complete Swagger/OpenAPI documentation + +--- + +## Authentication & Authorization + +### Authentication Methods + +#### Primary: JWT Bearer Token + +``` +Authorization: Bearer +``` + +**Token Characteristics:** +- Contains `user_id` and `account_id` +- Type: `access` (15-minute expiry) +- Automatically sets `request.account` via middleware +- Resolves account → tenant context automatically + +**Token Payload:** +```json +{ + "user_id": 1, + "account_id": 1, + "type": "access", + "exp": 1234567890 +} +``` + +#### Fallback Methods + +1. **Session Authentication** (admin panel) + - Class: `CSRFExemptSessionAuthentication` + - Use case: Django admin panel (`/admin/`) + +2. **Basic Authentication** (debug/testing) + - Class: `rest_framework.authentication.BasicAuthentication` + - Use case: API testing tools (Postman, curl) + +### Authentication Order + +1. JWT Token Authentication (tried first) +2. Session Authentication (fallback) +3. Basic Authentication (last fallback) +4. If all fail: 401 Unauthorized + +### Getting an Access Token + +**Login Endpoint:** +```http +POST /api/v1/auth/login/ +Content-Type: application/json + +{ + "email": "user@example.com", + "password": "your_password" +} +``` + +**Response:** +```json +{ + "success": true, + "data": { + "user": { + "id": 1, + "email": "user@example.com", + "username": "user", + "role": "owner", + "account": { ... } + }, + "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + }, + "request_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +### Token Expiration + +- **Access Token**: 15 minutes +- **Refresh Token**: 7 days + +**Refresh Token:** +```http +POST /api/v1/auth/refresh/ +Content-Type: application/json + +{ + "refresh": "your_refresh_token" +} +``` + +### Public Endpoints (No Authentication Required) + +- `POST /api/v1/auth/register/` - User registration +- `POST /api/v1/auth/login/` - User login +- `GET /api/v1/auth/plans/` - List plans +- `GET /api/v1/auth/industries/` - List industries +- `GET /api/v1/system/status/` - System health check +- `GET /api/v1/system/ping/` - Health check endpoint + +**All other endpoints require JWT authentication.** + +### Authorization Layers + +Every endpoint enforces layered authorization: + +1. **User Authentication**: User must be authenticated +2. **Tenant Access**: User must belong to the tenant/account +3. **Role Authorization**: User must have appropriate role +4. **Site/Sector Access**: User must have access to requested site/sector + +--- + +## Response Format Standard + +### Mandatory Format + +**This is the global standard for all endpoints - no exceptions.** + +### Success Response + +```json +{ + "success": true, + "data": { + "id": 1, + "name": "Example Keyword", + "status": "active" + }, + "message": "Optional human-readable success message", + "request_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +### Paginated Response + +```json +{ + "success": true, + "count": 120, + "next": "http://api.igny8.com/api/v1/planner/keywords/?page=3", + "previous": "http://api.igny8.com/api/v1/planner/keywords/?page=1", + "results": [ + {"id": 1, "name": "Keyword 1"}, + {"id": 2, "name": "Keyword 2"}, + ... + ], + "request_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +### Error Response + +```json +{ + "success": false, + "error": "Readable top-level error message", + "errors": { + "field_name": ["Field-specific error messages"] + }, + "request_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +### Response Helper Functions + +**File:** `backend/igny8_core/api/response.py` + +```python +from igny8_core.api.response import success_response, error_response, paginated_response + +# Success response +return success_response( + data={"id": 1, "name": "Example"}, + message="Resource created successfully", + status_code=status.HTTP_201_CREATED +) + +# Error response +return error_response( + error="Validation failed", + errors={"email": ["Invalid email format"]}, + status_code=status.HTTP_400_BAD_REQUEST +) + +# Paginated response +paginator = CustomPageNumberPagination() +page = paginator.paginate_queryset(queryset, request) +serializer = MySerializer(page, many=True) +paginated_data = paginator.get_paginated_response(serializer.data).data +return paginated_response(paginated_data, message="Resources retrieved successfully") +``` + +--- + +## Error Handling + +### HTTP Status Codes + +| Code | Meaning | Description | +|------|---------|-------------| +| 200 | OK | Request successful | +| 201 | Created | Resource created successfully | +| 204 | No Content | Resource deleted successfully | +| 400 | Bad Request | Validation error or invalid request | +| 401 | Unauthorized | Authentication required | +| 403 | Forbidden | Permission denied | +| 404 | Not Found | Resource not found | +| 409 | Conflict | Resource conflict (e.g., duplicate) | +| 422 | Unprocessable Entity | Validation failed | +| 429 | Too Many Requests | Rate limit exceeded | +| 500 | Internal Server Error | Server error | + +### Centralized Exception Handler + +**File:** `backend/igny8_core/api/exception_handlers.py` + +All exceptions are handled by a centralized exception handler that: +- Wraps all errors in unified format +- Uses proper HTTP status codes +- Includes sanitized validation errors under `errors` +- Always attaches `request_id` for error tracking +- Logs full exception details +- In DEBUG mode: includes traceback + request context + +### Error Response Examples + +**Validation Error (400):** +```json +{ + "success": false, + "error": "Validation failed", + "errors": { + "email": ["Invalid email format"], + "password": ["Password must be at least 8 characters"] + }, + "request_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +**Authentication Error (401):** +```json +{ + "success": false, + "error": "Authentication required", + "request_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +**Permission Error (403):** +```json +{ + "success": false, + "error": "Permission denied", + "request_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +**Rate Limit (429):** +```json +{ + "success": false, + "error": "Rate limit exceeded", + "request_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +### Server-side Logging + +- All 4xx errors logged as **warning** +- All 5xx errors logged as **error** +- Structured format with timestamp, request_id, endpoint, user_id, account_id, status_code, error_message +- Rotating log files +- Sentry integration hooks for production + +--- + +## Rate Limiting + +### Rate Limiting Configuration + +Rate limits are scoped by operation type. Check response headers for limit information: + +- `X-Throttle-Limit`: Maximum requests allowed +- `X-Throttle-Remaining`: Remaining requests in current window +- `X-Throttle-Reset`: Time when limit resets (Unix timestamp) + +### Rate Limit Scopes + +| Scope | Limit | Description | +|-------|-------|-------------| +| `ai_function` | 10/min | AI content generation, clustering | +| `image_gen` | 15/min | Image generation | +| `content_write` | 30/min | Content creation, updates | +| `content_read` | 100/min | Content listing, retrieval | +| `auth` | 20/min | Login, register, password reset | +| `auth_strict` | 5/min | Sensitive auth operations | +| `planner` | 60/min | Keyword, cluster, idea operations | +| `planner_ai` | 10/min | AI-powered planner operations | +| `writer` | 60/min | Task, content management | +| `writer_ai` | 10/min | AI-powered writer operations | +| `system` | 100/min | Settings, prompts, profiles | +| `system_admin` | 30/min | Admin-only system operations | +| `billing` | 30/min | Credit queries, usage logs | +| `billing_admin` | 10/min | Credit management (admin) | +| `default` | 100/min | Default for endpoints without scope | + +### Handling Rate Limits + +When rate limited (429), the response includes: +- Error message: "Rate limit exceeded" +- Headers with reset time +- Wait until `X-Throttle-Reset` before retrying + +**Example:** +```http +HTTP/1.1 429 Too Many Requests +X-Throttle-Limit: 60 +X-Throttle-Remaining: 0 +X-Throttle-Reset: 1700123456 +Retry-After: 60 + +{ + "success": false, + "error": "Rate limit exceeded", + "request_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +### Debug Mode Bypass + +Set `IGNY8_DEBUG_THROTTLE=True` or `DEBUG=True` to bypass throttling in development. + +--- + +## Pagination + +### Pagination Configuration + +**Default Settings:** +- Default page size: **10** +- Maximum page size: **100** +- Query parameter: `page_size` (optional) +- Page parameter: `page` (default: 1) + +### Query Parameters + +**Pagination:** +``` +?page=2&page_size=25 +``` + +**Filtering:** +``` +?status=active +?site_id=1 +?sector_id=2 +?cluster_id=5 +``` + +**Search:** +``` +?search=keyword +``` + +**Ordering:** +``` +?ordering=-created_at +?ordering=name,status +``` + +### Pagination Response Format + +```json +{ + "success": true, + "count": 150, + "next": "http://api.igny8.com/api/v1/planner/keywords/?page=3&page_size=25", + "previous": "http://api.igny8.com/api/v1/planner/keywords/?page=1&page_size=25", + "results": [ + // Array of results + ], + "request_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +### Pagination Fields + +- `count`: Total number of items +- `next`: URL to next page (null if last page) +- `previous`: URL to previous page (null if first page) +- `results`: Array of items for current page + +--- + +## Roles & Permissions + +### Role Hierarchy + +``` +owner > admin > editor > viewer > system_bot +``` + +### Standard Permission Classes + +**File:** `backend/igny8_core/api/permissions.py` + +| Permission Class | Description | Use Case | +|-----------------|------------|----------| +| `IsAuthenticatedAndActive` | User authenticated and active | Base permission for most endpoints | +| `HasTenantAccess` | User belongs to tenant/account | Tenant isolation | +| `IsViewerOrAbove` | Viewer, editor, admin, or owner | Read-only operations | +| `IsEditorOrAbove` | Editor, admin, or owner | Content operations | +| `IsAdminOrOwner` | Admin or owner only | Settings, keys, billing | + +### Permission Matrix by Endpoint Type + +| Endpoint Type | Required Permissions | Roles Allowed | +|--------------|---------------------|---------------| +| Public (register, login) | `AllowAny` | Anyone | +| Read-only (list, retrieve) | `IsAuthenticatedAndActive` + `HasTenantAccess` | All authenticated users | +| Content operations | `IsAuthenticatedAndActive` + `HasTenantAccess` + `IsEditorOrAbove` | Editor, Admin, Owner | +| User management | `IsAuthenticatedAndActive` + `HasTenantAccess` + `IsAdminOrOwner` | Admin, Owner | +| Billing/Transactions | `IsAuthenticatedAndActive` + `HasTenantAccess` + `IsAdminOrOwner` | Admin, Owner | +| Integration settings | `IsAuthenticatedAndActive` + `HasTenantAccess` + `IsAdminOrOwner` | Admin, Owner | + +--- + +## Tenant / Site / Sector Scoping + +### Scoping Rules + +Every resource created or fetched must be scoped by: + +1. **Account/Tenant** - User's account +2. **Site** - Specific site within account +3. **Sector** - Specific sector within site + +### Enforcement + +**Base Classes:** +- `AccountModelViewSet` - Handles account isolation +- `SiteSectorModelViewSet` - Filters queries by site/sector + +**Requirements:** +- All custom actions must use `.get_queryset()` to avoid bypassing filters +- Any ID list must be verified to belong to the authenticated tenant +- Site/sector access validated based on user role + +### Scoping Example + +```python +class KeywordViewSet(SiteSectorModelViewSet): + # Automatically filters by: + # 1. account (from request.account) + # 2. site_id (from query params or request) + # 3. sector_id (from query params or request) + + queryset = Keyword.objects.all() + serializer_class = KeywordSerializer + + def get_queryset(self): + # Base class handles account/site/sector filtering + queryset = super().get_queryset() + # Additional filtering can be added here + return queryset +``` + +--- + +## Complete Endpoint Reference + +### Authentication Endpoints + +**Base Path**: `/api/v1/auth/` + +#### POST `/api/v1/auth/register/` +**Purpose**: User registration +**Authentication**: None (AllowAny) + +**Request:** +```json +{ + "email": "user@example.com", + "password": "password123", + "password_confirm": "password123" +} +``` + +**Response:** +```json +{ + "success": true, + "data": { + "user": { + "id": 1, + "email": "user@example.com", + "role": "owner", + "account": { ... } + } + }, + "message": "Registration successful" +} +``` + +#### POST `/api/v1/auth/login/` +**Purpose**: User login +**Authentication**: None (AllowAny) + +**Request:** +```json +{ + "email": "user@example.com", + "password": "password123" +} +``` + +**Response:** +```json +{ + "success": true, + "data": { + "user": { ... }, + "access": "eyJ0eXAiOiJKV1QiLCJhbGc...", + "refresh": "eyJ0eXAiOiJKV1QiLCJhbGc..." + }, + "message": "Login successful" +} +``` + +#### POST `/api/v1/auth/refresh/` +**Purpose**: Refresh access token +**Authentication**: None (requires refresh token) + +**Request:** +```json +{ + "refresh": "your_refresh_token" +} +``` + +#### POST `/api/v1/auth/change-password/` +**Purpose**: Change user password +**Authentication**: Required (IsAuthenticated) + +**Request:** +```json +{ + "old_password": "oldpass123", + "new_password": "newpass123", + "new_password_confirm": "newpass123" +} +``` + +#### GET `/api/v1/auth/me/` +**Purpose**: Get current user information +**Authentication**: Required (IsAuthenticated) + +**Response:** +```json +{ + "success": true, + "data": { + "user": { + "id": 1, + "email": "user@example.com", + "role": "owner", + "account": { ... }, + "accessible_sites": [ ... ] + } + } +} +``` + +### User Management Endpoints + +**Base Path**: `/api/v1/auth/users/` +**Permission**: IsOwnerOrAdmin + +**Standard CRUD:** +- `GET /api/v1/auth/users/` - List users +- `POST /api/v1/auth/users/` - Create user +- `GET /api/v1/auth/users/{id}/` - Get user +- `PUT /api/v1/auth/users/{id}/` - Update user +- `DELETE /api/v1/auth/users/{id}/` - Delete user + +**Custom Actions:** +- `POST /api/v1/auth/users/invite/` - Invite user +- `POST /api/v1/auth/users/{id}/activate/` - Activate user + +### Account Management Endpoints + +**Base Path**: `/api/v1/auth/accounts/` +**Permission**: IsOwnerOrAdmin + +**Standard CRUD:** +- `GET /api/v1/auth/accounts/` - List accounts +- `POST /api/v1/auth/accounts/` - Create account +- `GET /api/v1/auth/accounts/{id}/` - Get account +- `PUT /api/v1/auth/accounts/{id}/` - Update account +- `DELETE /api/v1/auth/accounts/{id}/` - Delete account + +### Site Management Endpoints + +**Base Path**: `/api/v1/auth/sites/` +**Permission**: IsEditorOrAbove + +**Standard CRUD:** +- `GET /api/v1/auth/sites/` - List sites +- `POST /api/v1/auth/sites/` - Create site +- `GET /api/v1/auth/sites/{id}/` - Get site +- `PUT /api/v1/auth/sites/{id}/` - Update site +- `DELETE /api/v1/auth/sites/{id}/` - Delete site + +**Custom Actions:** +- `GET /api/v1/auth/sites/{id}/sectors/` - Get site sectors +- `POST /api/v1/auth/sites/{id}/set_active/` - Set active site +- `POST /api/v1/auth/sites/{id}/select_sectors/` - Select sectors + +### Sector Management Endpoints + +**Base Path**: `/api/v1/auth/sectors/` +**Permission**: IsEditorOrAbove + +**Standard CRUD:** +- `GET /api/v1/auth/sectors/` - List sectors +- `POST /api/v1/auth/sectors/` - Create sector +- `GET /api/v1/auth/sectors/{id}/` - Get sector +- `PUT /api/v1/auth/sectors/{id}/` - Update sector +- `DELETE /api/v1/auth/sectors/{id}/` - Delete sector + +### Planner Module Endpoints + +**Base Path**: `/api/v1/planner/` + +#### Keyword Management + +**Base Path**: `/api/v1/planner/keywords/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess +**Inherits**: SiteSectorModelViewSet + +**Standard CRUD:** +- `GET /api/v1/planner/keywords/` - List keywords (paginated) +- `POST /api/v1/planner/keywords/` - Create keyword +- `GET /api/v1/planner/keywords/{id}/` - Get keyword +- `PUT /api/v1/planner/keywords/{id}/` - Update keyword +- `DELETE /api/v1/planner/keywords/{id}/` - Delete keyword + +**Filtering:** +- `status` - Filter by status +- `cluster_id` - Filter by cluster +- `seed_keyword__intent` - Filter by intent +- `seed_keyword_id` - Filter by seed keyword ID +- `difficulty_min`, `difficulty_max` - Difficulty range +- `volume_min`, `volume_max` - Volume range +- `site_id` - Filter by site (query param) +- `sector_id` - Filter by sector (query param) + +**Search:** +- `search` - Search by keyword text + +**Ordering:** +- `ordering` - Order by `created_at`, `seed_keyword__volume`, `seed_keyword__difficulty` +- Default: `-created_at` (newest first) + +**Custom Actions:** +- `POST /api/v1/planner/keywords/bulk_delete/` - Bulk delete keywords + - Request: `{ "ids": [1, 2, 3] }` +- `POST /api/v1/planner/keywords/bulk_update_status/` - Bulk update status + - Request: `{ "ids": [1, 2, 3], "status": "active" }` +- `POST /api/v1/planner/keywords/bulk_add_from_seed/` - Add keywords from seed library + - Request: `{ "seed_keyword_ids": [1, 2, 3], "site_id": 1, "sector_id": 1 }` +- `GET /api/v1/planner/keywords/export/` - Export keywords to CSV +- `POST /api/v1/planner/keywords/import_keywords/` - Import keywords from CSV +- `POST /api/v1/planner/keywords/auto_cluster/` - Auto-cluster keywords using AI + - Request: `{ "ids": [1, 2, 3, ...], "sector_id": 1 }` + - Max Keywords: 20 per batch + - Returns: Celery task ID for progress tracking + +#### Cluster Management + +**Base Path**: `/api/v1/planner/clusters/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess +**Inherits**: SiteSectorModelViewSet + +**Standard CRUD:** +- `GET /api/v1/planner/clusters/` - List clusters +- `POST /api/v1/planner/clusters/` - Create cluster +- `GET /api/v1/planner/clusters/{id}/` - Get cluster +- `PUT /api/v1/planner/clusters/{id}/` - Update cluster +- `DELETE /api/v1/planner/clusters/{id}/` - Delete cluster + +**Filtering:** +- `status` - Filter by status +- `site_id` - Filter by site (query param) +- `sector_id` - Filter by sector (query param) + +**Search:** +- `search` - Search by cluster name + +**Custom Actions:** +- `POST /api/v1/planner/clusters/bulk_delete/` - Bulk delete clusters +- `POST /api/v1/planner/clusters/auto_generate_ideas/` - Auto-generate content ideas + - Request: `{ "ids": [1] }` (max 1 cluster per batch) + - Returns: Celery task ID for progress tracking + +#### Content Ideas Management + +**Base Path**: `/api/v1/planner/ideas/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess +**Inherits**: SiteSectorModelViewSet + +**Standard CRUD:** +- `GET /api/v1/planner/ideas/` - List content ideas +- `POST /api/v1/planner/ideas/` - Create content idea +- `GET /api/v1/planner/ideas/{id}/` - Get content idea +- `PUT /api/v1/planner/ideas/{id}/` - Update content idea +- `DELETE /api/v1/planner/ideas/{id}/` - Delete content idea + +**Filtering:** +- `status` - Filter by status +- `cluster_id` - Filter by cluster +- `content_type` - Filter by content type +- `site_id` - Filter by site (query param) +- `sector_id` - Filter by sector (query param) + +**Custom Actions:** +- `POST /api/v1/planner/ideas/bulk_delete/` - Bulk delete ideas +- `POST /api/v1/planner/ideas/bulk_queue_to_writer/` - Queue ideas to writer (create tasks) + - Request: `{ "ids": [1, 2, 3] }` + +### Writer Module Endpoints + +**Base Path**: `/api/v1/writer/` + +#### Task Management + +**Base Path**: `/api/v1/writer/tasks/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess +**Inherits**: SiteSectorModelViewSet + +**Standard CRUD:** +- `GET /api/v1/writer/tasks/` - List tasks +- `POST /api/v1/writer/tasks/` - Create task +- `GET /api/v1/writer/tasks/{id}/` - Get task +- `PUT /api/v1/writer/tasks/{id}/` - Update task +- `DELETE /api/v1/writer/tasks/{id}/` - Delete task + +**Filtering:** +- `status` - Filter by status (draft, in_progress, review, completed, archived) +- `cluster_id` - Filter by cluster +- `content_type` - Filter by content type +- `content_structure` - Filter by content structure +- `site_id` - Filter by site (query param) +- `sector_id` - Filter by sector (query param) + +**Search:** +- `search` - Search by title or keywords + +**Custom Actions:** +- `POST /api/v1/writer/tasks/bulk_delete/` - Bulk delete tasks +- `POST /api/v1/writer/tasks/bulk_update/` - Bulk update task status +- `POST /api/v1/writer/tasks/auto_generate_content/` - Auto-generate content using AI + - Request: `{ "ids": [1, 2, 3, ...] }` (max 50 tasks per batch) + - Returns: Celery task ID for progress tracking + +#### Content Management + +**Base Path**: `/api/v1/writer/content/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess +**Inherits**: SiteSectorModelViewSet + +**Standard CRUD:** +- `GET /api/v1/writer/content/` - List content +- `POST /api/v1/writer/content/` - Create content +- `GET /api/v1/writer/content/{id}/` - Get content +- `PUT /api/v1/writer/content/{id}/` - Update content +- `DELETE /api/v1/writer/content/{id}/` - Delete content + +**Filtering:** +- `status` - Filter by status +- `content_type` - Filter by content type +- `site_id` - Filter by site (query param) +- `sector_id` - Filter by sector (query param) + +**Custom Actions:** +- `POST /api/v1/writer/content/generate_image_prompts/` - Generate image prompts from content + - Request: `{ "ids": [1, 2, 3] }` + - Returns: Celery task ID for progress tracking + +#### Image Management + +**Base Path**: `/api/v1/writer/images/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess +**Inherits**: SiteSectorModelViewSet + +**Standard CRUD:** +- `GET /api/v1/writer/images/` - List images +- `POST /api/v1/writer/images/` - Create image +- `GET /api/v1/writer/images/{id}/` - Get image +- `PUT /api/v1/writer/images/{id}/` - Update image +- `DELETE /api/v1/writer/images/{id}/` - Delete image + +**Filtering:** +- `image_type` - Filter by type (featured, in_article, desktop, mobile) +- `status` - Filter by status +- `content_id` - Filter by content +- `task_id` - Filter by task +- `site_id` - Filter by site (query param) +- `sector_id` - Filter by sector (query param) + +**Custom Actions:** +- `GET /api/v1/writer/images/{id}/file/` - Get image file URL +- `GET /api/v1/writer/images/content_images/` - Get images for content + - Query Params: `content_id` (required) +- `POST /api/v1/writer/images/generate_images/` - Generate images using AI + - Request: `{ "ids": [1, 2, 3, ...] }` + - Returns: Celery task ID for progress tracking +- `POST /api/v1/writer/images/bulk_update/` - Bulk update image status + +### System Module Endpoints + +**Base Path**: `/api/v1/system/` + +#### AI Prompt Management + +**Base Path**: `/api/v1/system/prompts/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess +**Inherits**: AccountModelViewSet + +**Standard CRUD:** +- `GET /api/v1/system/prompts/` - List prompts +- `POST /api/v1/system/prompts/` - Create prompt +- `GET /api/v1/system/prompts/{id}/` - Get prompt +- `PUT /api/v1/system/prompts/{id}/` - Update prompt +- `DELETE /api/v1/system/prompts/{id}/` - Delete prompt + +**Custom Actions:** +- `GET /api/v1/system/prompts/by_type/{prompt_type}/` - Get prompt by type +- `POST /api/v1/system/prompts/save/` - Save prompt (requires editor/admin) +- `POST /api/v1/system/prompts/reset/` - Reset prompt to default + +#### Integration Settings + +**Base Path**: `/api/v1/system/settings/integrations/` +**Permission**: IsAdminOrOwner + +**Custom URL Patterns:** +- `GET /api/v1/system/settings/integrations/{pk}/` - Get integration settings +- `POST /api/v1/system/settings/integrations/{pk}/save/` - Save integration settings +- `PUT /api/v1/system/settings/integrations/{pk}/` - Update integration settings +- `POST /api/v1/system/settings/integrations/{pk}/test/` - Test connection + - Request: `{ "provider": "openai" }` or `{ "provider": "runware" }` +- `POST /api/v1/system/settings/integrations/{pk}/generate/` - Test image generation +- `GET /api/v1/system/settings/task_progress/{task_id}/` - Get Celery task progress +- `GET /api/v1/system/integrations/image_generation/` - Get image generation settings + +#### System Status + +**Base Path**: `/api/v1/system/` + +- `GET /api/v1/system/status/` - System health check (AllowAny) +- `GET /api/v1/system/ping/` - Health check endpoint (AllowAny) +- `GET /api/v1/system/request-metrics/{request_id}/` - Get request metrics for debugging + +### Admin Billing & Credits (Admin-only, Unified) + +**Base Path**: `/api/v1/admin/billing/` (all admin billing/credits live here) + +- `GET /api/v1/admin/billing/stats/` - System billing stats (admin-only) +- `GET /api/v1/admin/billing/invoices/` - Admin invoice listing (all accounts) +- `GET /api/v1/admin/billing/payments/` - Admin payment listing (all accounts) +- `GET /api/v1/admin/billing/pending_payments/` - Pending manual payments (admin review queue) +- `POST /api/v1/admin/billing/{id}/approve_payment/` - Approve manual payment (admin-only) +- `POST /api/v1/admin/billing/{id}/reject_payment/` - Reject manual payment (admin-only) + +- `GET /api/v1/admin/billing/payment-method-configs/` - List payment method configs (country-level); query: `country_code`, `payment_method` +- `POST /api/v1/admin/billing/payment-method-configs/` - Create payment method config +- `GET /api/v1/admin/billing/payment-method-configs/{id}/` - Retrieve payment method config +- `PATCH/PUT /api/v1/admin/billing/payment-method-configs/{id}/` - Update payment method config +- `DELETE /api/v1/admin/billing/payment-method-configs/{id}/` - Delete payment method config +- `GET /api/v1/admin/billing/account-payment-methods/` - List account payment methods (query `account_id` to scope) +- `POST /api/v1/admin/billing/account-payment-methods/` - Create account payment method +- `GET /api/v1/admin/billing/account-payment-methods/{id}/` - Retrieve account payment method +- `PATCH/PUT /api/v1/admin/billing/account-payment-methods/{id}/` - Update account payment method +- `DELETE /api/v1/admin/billing/account-payment-methods/{id}/` - Delete account payment method +- `POST /api/v1/admin/billing/account-payment-methods/{id}/set_default/` - Set default account payment method +- `GET /api/v1/admin/credit-costs/` - List credit cost configurations (admin-only) +- `POST /api/v1/admin/credit-costs/` - Update credit cost configurations (admin-only) +- `GET /api/v1/admin/users/` - List users/accounts with credit info (admin-only) +- `POST /api/v1/admin/users/{user_id}/adjust-credits/` - Adjust user credits (admin-only) + +> Non-standard/legacy endpoints to deprecate and remove (do not use): +> - `/api/v1/billing/admin/stats/` +> - `/api/v1/billing/admin/invoices/` +> - `/api/v1/billing/admin/payments/` +> - `/api/v1/billing/admin/pending_payments/` +> - `/api/v1/billing/admin/{id}/approve_payment/` +> - `/api/v1/billing/admin/{id}/reject_payment/` + + +### Billing Module Endpoints + +**Base Path**: `/api/v1/billing/` + +#### Credit Balance + +**Base Path**: `/api/v1/billing/credits/balance/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess + +- `GET /api/v1/billing/credits/balance/` - Get credit balance + +**Response:** +```json +{ + "success": true, + "data": { + "credits": 1000, + "plan_credits_per_month": 500, + "credits_used_this_month": 250, + "credits_remaining": 750 + } +} +``` + +#### Credit Usage + +**Base Path**: `/api/v1/billing/credits/usage/` +**Permission**: IsAuthenticatedAndActive + HasTenantAccess + +**Standard CRUD:** +- `GET /api/v1/billing/credits/usage/` - List usage logs (paginated) +- `GET /api/v1/billing/credits/usage/{id}/` - Get usage log + +**Filtering:** +- `operation_type` - Filter by operation type +- `start_date` - Filter by start date (YYYY-MM-DD) +- `end_date` - Filter by end date (YYYY-MM-DD) + +**Custom Actions:** +- `GET /api/v1/billing/credits/usage/summary/` - Get usage summary +- `GET /api/v1/billing/credits/usage/limits/` - Get usage limits + +#### Credit Transactions + +**Base Path**: `/api/v1/billing/credits/transactions/` +**Permission**: IsAdminOrOwner + +**Standard CRUD:** +- `GET /api/v1/billing/credits/transactions/` - List transactions (paginated) +- `GET /api/v1/billing/credits/transactions/{id}/` - Get transaction + +**Filtering:** +- `transaction_type` - Filter by type +- `start_date` - Filter by start date +- `end_date` - Filter by end date + +#### Credit Packages + +**Base Path**: `/api/v1/billing/credit-packages/` +**Permission**: IsAuthenticated + +- `GET /api/v1/billing/credit-packages/` - List available credit packages +- `POST /api/v1/billing/credit-packages/{id}/purchase/` - Purchase a credit package + +#### Invoices + +**Base Path**: `/api/v1/billing/invoices/` +**Permission**: IsAuthenticated + +- `GET /api/v1/billing/invoices/` - List invoices +- `GET /api/v1/billing/invoices/{id}/` - Get invoice detail +- `GET /api/v1/billing/invoices/{id}/download_pdf/` - Download invoice PDF + +#### Payment Methods + +**Base Path**: `/api/v1/billing/payment-methods/` +**Permission**: IsAuthenticated + +- `GET /api/v1/billing/payment-methods/` - List payment methods +- `POST /api/v1/billing/payment-methods/` - Create payment method +- `GET /api/v1/billing/payment-methods/{id}/` - Get payment method +- `PUT /api/v1/billing/payment-methods/{id}/` - Update payment method +- `PATCH /api/v1/billing/payment-methods/{id}/` - Partial update payment method +- `DELETE /api/v1/billing/payment-methods/{id}/` - Delete payment method +- `POST /api/v1/billing/payment-methods/{id}/set_default/` - Set default payment method +- `GET /api/v1/billing/payment-methods/available/` - List available payment methods (config-driven) + +#### Payments + +**Base Path**: `/api/v1/billing/payments/` +**Permission**: IsAuthenticated + +- `GET /api/v1/billing/payments/` - List payments +- `POST /api/v1/billing/payments/manual/` - Submit manual payment for approval + +#### Transactions (alias-free) + +**Base Path**: `/api/v1/billing/transactions/` +**Permission**: IsAuthenticated + +- `GET /api/v1/billing/transactions/` - List transactions (with current balance included) + +--- + +## Integration Examples + +### Python + +```python +import requests + +BASE_URL = "https://api.igny8.com/api/v1" + +# Login +response = requests.post( + f"{BASE_URL}/auth/login/", + json={"email": "user@example.com", "password": "password"} +) +data = response.json() + +if data['success']: + token = data['data']['access'] + + # Use token for authenticated requests + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + + # Get keywords + response = requests.get( + f"{BASE_URL}/planner/keywords/?page=1&page_size=25", + headers=headers + ) + result = response.json() + + if result['success']: + keywords = result['results'] + print(f"Found {result['count']} keywords") + else: + print(f"Error: {result['error']}") +``` + +### JavaScript + +```javascript +const BASE_URL = 'https://api.igny8.com/api/v1'; + +// Login +const loginResponse = await fetch(`${BASE_URL}/auth/login/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email: 'user@example.com', + password: 'password' + }) +}); + +const loginData = await loginResponse.json(); + +if (loginData.success) { + const token = loginData.data.access; + + // Use token for authenticated requests + const headers = { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }; + + // Get keywords + const keywordsResponse = await fetch( + `${BASE_URL}/planner/keywords/?page=1&page_size=25`, + { headers } + ); + + const keywordsData = await keywordsResponse.json(); + + if (keywordsData.success) { + const keywords = keywordsData.results; + console.log(`Found ${keywordsData.count} keywords`); + } else { + console.error('Error:', keywordsData.error); + } +} +``` + +### cURL + +```bash +# Login +curl -X POST https://api.igny8.com/api/v1/auth/login/ \ + -H "Content-Type: application/json" \ + -d '{"email":"user@example.com","password":"password"}' + +# Get keywords (with token) +curl -X GET https://api.igny8.com/api/v1/planner/keywords/ \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "Content-Type: application/json" +``` + +### Frontend Integration (TypeScript) + +```typescript +// Using fetchAPI wrapper (automatically extracts data from unified format) +import { fetchAPI } from './services/api'; + +// List keywords +const response = await fetchAPI('/v1/planner/keywords/?page=1&page_size=25'); +// response is already extracted: { count, next, previous, results } + +// Create keyword +const newKeyword = await fetchAPI('/v1/planner/keywords/', { + method: 'POST', + body: JSON.stringify({ + keyword: 'example keyword', + site_id: 1, + sector_id: 2, + status: 'active' + }) +}); + +// Error handling +try { + const data = await fetchAPI('/v1/planner/keywords/'); +} catch (error) { + // Error is already parsed from unified format + console.error(error.message); + if (error.errors) { + // Handle field-specific errors + Object.keys(error.errors).forEach(field => { + console.error(`${field}: ${error.errors[field].join(', ')}`); + }); + } +} +``` + +### WordPress Plugin Integration (PHP) + +```php +class Igny8API { + private $base_url = 'https://api.igny8.com/api/v1'; + private $token; + + public function login($email, $password) { + $response = wp_remote_post($this->base_url . '/auth/login/', [ + 'headers' => ['Content-Type' => 'application/json'], + 'body' => json_encode(['email' => $email, 'password' => $password]) + ]); + + $data = json_decode(wp_remote_retrieve_body($response), true); + + if ($data['success']) { + $this->token = $data['data']['access']; + return true; + } + + return false; + } + + public function getKeywords($page = 1, $page_size = 25) { + $response = wp_remote_get( + $this->base_url . '/planner/keywords/?page=' . $page . '&page_size=' . $page_size, + [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $this->token, + 'Content-Type' => 'application/json' + ] + ] + ); + + $data = json_decode(wp_remote_retrieve_body($response), true); + + if ($data['success']) { + return $data['results']; + } + + return []; + } +} +``` + +--- + +## Testing & Debugging + +### Request ID Tracking + +Every API request includes a unique `request_id` in the response. Use this ID for: +- Debugging issues +- Log correlation +- Support requests + +The `request_id` is included in: +- All success responses +- All error responses +- Response headers (`X-Request-ID`) + +### Progress Tracking (AI Functions) + +All AI functions return a Celery task ID for progress tracking: + +**Request:** +```json +POST /api/v1/planner/keywords/auto_cluster/ +{ + "ids": [1, 2, 3, 4, 5], + "sector_id": 1 +} +``` + +**Response:** +```json +{ + "success": true, + "data": { + "task_id": "abc123-def456-ghi789" + } +} +``` + +**Poll Progress:** +```json +GET /api/v1/system/settings/task_progress/abc123-def456-ghi789/ +``` + +**Progress Response:** +```json +{ + "state": "PROGRESS", + "meta": { + "phase": "AI_CALL", + "percentage": 50, + "message": "Processing keywords...", + "request_steps": [...], + "response_steps": [...], + "cost": 0.05, + "tokens": 1500 + } +} +``` + +### Error Handling Best Practices + +**401 Unauthorized**: Trigger logout → redirect to login +**403 Forbidden**: Show permission alert +**429 Too Many Requests**: Show rate limit warning with retry time +**4xx/5xx**: Display error message from `error` field + +### Rate Limit Monitoring + +```typescript +const throttleLimit = response.headers.get('X-Throttle-Limit'); +const throttleRemaining = response.headers.get('X-Throttle-Remaining'); + +if (parseInt(throttleRemaining) < 5) { + showNotification('Approaching rate limit', 'warning'); +} +``` + +--- + +## Change Management + +### Versioning Strategy + +- **v1** remains stable long-term +- Breaking changes require **v2** +- Deprecations allowed only with explicit timeline +- Non-breaking changes can be added to v1 + +### Breaking Changes + +**Definition:** Changes that require client code updates + +**Examples:** +- Removing an endpoint +- Changing response structure +- Changing authentication method +- Removing a field from response + +**Process:** +1. Document breaking change +2. Provide migration guide +3. Deprecate in v1 with timeline +4. Implement in v2 +5. Maintain v1 for deprecation period + +### Non-Breaking Changes + +**Definition:** Changes that don't require client code updates + +**Examples:** +- Adding new endpoints +- Adding optional fields to response +- Adding new query parameters +- Performance improvements + +### Changelog + +All API changes are documented in `CHANGELOG.md` with: +- Version number +- Date +- Type (Added, Changed, Fixed, Deprecated, Removed, Security) +- Description +- Affected areas +- Migration notes (if applicable) + +### Endpoint Change & Documentation Checklist (Unified API) + +1) Design +- Map to an existing module/tag; if new, add to Module Namespaces and Tag Map. +- Choose path under the correct base (`/api/v1/{module}/...`); avoid new sub-namespaces unless justified. + +2) Implement +- Use unified response helpers and proper permissions/rate limits. +- Add `extend_schema` tags matching the module tag. + +3) Schema & Docs +- Ensure swagger tag exists in `SPECTACULAR_SETTINGS` with the agreed order. +- Regenerate/reload the API (server restart) so `/api/schema/` reflects changes. +- Verify in Swagger UI (`/api/docs/`) and ReDoc (`/api/redoc/`) that the operation is under the right tag. + +4) Reference Updates +- Update this reference file with the new endpoint(s) under the correct module section. +- Update `CHANGELOG.md` (type, summary, impacted clients). + +5) Deprecation (if applicable) +- Mark legacy routes, add timeline, and keep compatibility shims only temporarily. + +--- + +## Summary + +### Implementation Status + +✅ **100% Complete** - All endpoints implemented with: +- Unified response format (`{success, data, message, errors, request_id}`) +- Proper authentication and authorization (JWT Bearer tokens) +- Rate limiting configured (scoped by operation type) +- Error handling standardized (centralized exception handler) +- Request ID tracking (every request has unique ID) +- Complete Swagger/OpenAPI documentation +- Tenant/site/sector scoping (automatic filtering) +- Pagination standardized (default: 10, max: 100) + +### Key Features + +- **100+ endpoints** across 5 modules +- **Unified response format** for all endpoints +- **Scoped rate limiting** (10-100 requests/minute depending on operation) +- **Layered authorization** (Authentication → Tenant → Role → Site/Sector) +- **Complete OpenAPI documentation** (Swagger UI, ReDoc) +- **Request ID tracking** for debugging +- **Progress tracking** for AI functions +- **Comprehensive error handling** with clear messages + +### Access Points + +- **Interactive Documentation**: `https://api.igny8.com/api/docs/` +- **ReDoc**: `https://api.igny8.com/api/redoc/` +- **OpenAPI Schema**: `https://api.igny8.com/api/schema/` + +--- + +**Last Updated**: 2025-01-XX +**API Version**: 1.0.0 +**Status**: ✅ **100% IMPLEMENTED** + diff --git a/docs/CHANGELOG.md b/old-docs/CHANGELOG.md similarity index 100% rename from docs/CHANGELOG.md rename to old-docs/CHANGELOG.md diff --git a/docs/README.md b/old-docs/README.md similarity index 100% rename from docs/README.md rename to old-docs/README.md diff --git a/old-docs/ai/AI-FUNCTIONS-COMPLETE-REFERENCE.md b/old-docs/ai/AI-FUNCTIONS-COMPLETE-REFERENCE.md new file mode 100644 index 00000000..cd466d0c --- /dev/null +++ b/old-docs/ai/AI-FUNCTIONS-COMPLETE-REFERENCE.md @@ -0,0 +1,1155 @@ +# IGNY8 AI Functions - Complete Technical Reference +**Date:** December 3, 2025 +**Version:** 2.0 - CORRECTED AFTER AUTOMATION AUDIT +**100% Based on Actual Codebase (Backend + Frontend + Automation Integration)** + +--- + +## Table of Contents +1. [Overview](#overview) +2. [AI Architecture](#ai-architecture) +3. [AI Function Registry](#ai-function-registry) +4. [Planner Module AI Functions](#planner-module-ai-functions) +5. [Writer Module AI Functions](#writer-module-ai-functions) +6. [AI Function Base Class](#ai-function-base-class) +7. [AI Engine & Execution](#ai-engine--execution) +8. [Credit System Integration](#credit-system-integration) +9. [Progress Tracking](#progress-tracking) + +--- + +## Overview + +IGNY8 uses a centralized AI function architecture where all AI operations inherit from `BaseAIFunction` and execute through `AIEngine`. This ensures consistent: +- Credit management +- Progress tracking +- Error handling +- Logging +- Response parsing + +**Total AI Functions: 6** + +| Function | Module | Purpose | Input | Output | Credits | +|----------|--------|---------|-------|--------|---------| +| `auto_cluster` | Planner | Group keywords into semantic clusters | Keyword IDs | Clusters created | ~1 per 5 keywords | +| `generate_ideas` | Planner | Generate content ideas from clusters | Cluster IDs | Ideas created | 2 per cluster | +| `generate_content` | Writer | Generate article content from tasks | Task IDs | Content drafts | ~5 per 2500 words | +| `generate_image_prompts` | Writer | Extract image prompts from content | Content IDs | Image records with prompts | ~2 per content | +| `generate_images` | Writer | Generate actual images from prompts | Image IDs | Image URLs | 1-4 per image | +| `optimize_content` | Writer | SEO optimization of content | Content IDs | Updated content | ~1 per content | + +--- + +## AI Architecture + +### Directory Structure + +``` +backend/igny8_core/ai/ +├── __init__.py +├── base.py # BaseAIFunction (abstract class) +├── engine.py # AIEngine (execution orchestrator) +├── registry.py # Function registration & lazy loading +├── ai_core.py # Core AI API interactions +├── prompts.py # PromptRegistry +├── tasks.py # Celery tasks for async execution +├── models.py # AITaskLog, AIUsageLog +├── validators.py # Input validation helpers +├── settings.py # AI configuration +├── tracker.py # ProgressTracker, StepTracker +└── functions/ + ├── __init__.py + ├── auto_cluster.py # AutoClusterFunction + ├── generate_ideas.py # GenerateIdeasFunction + ├── generate_content.py # GenerateContentFunction + ├── generate_image_prompts.py # GenerateImagePromptsFunction + ├── generate_images.py # GenerateImagesFunction + └── optimize_content.py # OptimizeContentFunction +``` + +### Execution Flow + +``` +User Action → API Endpoint → Service Layer → AIEngine.execute() + ↓ + BaseAIFunction + ↓ + ┌───────────────────┴───────────────────┐ + ↓ ↓ + Synchronous (small ops) Async (Celery task) + ↓ ↓ + Direct function execution run_ai_task.delay() + ↓ ↓ + INIT → PREP → AI_CALL → PARSE → SAVE → DONE + ↓ + Credit deduction (automatic) + ↓ + Progress tracking (StepTracker) + ↓ + AIUsageLog created +``` + +--- + +## AI Function Registry + +**File:** `backend/igny8_core/ai/registry.py` + +### Lazy Loading System + +Functions are registered with lazy loaders and only imported when called: + +```python +_FUNCTION_REGISTRY: Dict[str, Type[BaseAIFunction]] = {} +_FUNCTION_LOADERS: Dict[str, callable] = {} + +def get_function_instance(name: str) -> Optional[BaseAIFunction]: + """Get function instance by name - lazy loads if needed""" + actual_name = FUNCTION_ALIASES.get(name, name) + fn_class = get_function(actual_name) + if fn_class: + return fn_class() + return None +``` + +### Registered Functions + +```python +# Lazy loaders +register_lazy_function('auto_cluster', _load_auto_cluster) +register_lazy_function('generate_ideas', _load_generate_ideas) +register_lazy_function('generate_content', _load_generate_content) +register_lazy_function('generate_images', _load_generate_images) +register_lazy_function('generate_image_prompts', _load_generate_image_prompts) +register_lazy_function('optimize_content', _load_optimize_content) +``` + +--- + +## Planner Module AI Functions + +### 1. AutoClusterFunction + +**File:** `backend/igny8_core/ai/functions/auto_cluster.py` + +**Purpose:** Groups semantically related keywords into topic clusters using AI + +**Class Definition:** +```python +class AutoClusterFunction(BaseAIFunction): + def get_name(self) -> str: + return 'auto_cluster' +``` + +**Metadata:** +```python +{ + 'display_name': 'Auto Cluster Keywords', + 'description': 'Group related keywords into semantic clusters using AI', + 'phases': { + 'INIT': 'Initializing clustering...', + 'PREP': 'Loading keywords...', + 'AI_CALL': 'Analyzing keyword relationships...', + 'PARSE': 'Parsing cluster data...', + 'SAVE': 'Creating clusters...', + 'DONE': 'Clustering complete!' + } +} +``` + +**Method: validate()** +```python +def validate(self, payload: dict, account=None) -> Dict: + # Validates: + # - IDs exist + # - Keywords exist in database + # - Account ownership + # NO MAX LIMIT - processes any count + return {'valid': True} +``` + +**Method: prepare()** +```python +def prepare(self, payload: dict, account=None) -> Dict: + ids = payload.get('ids', []) + sector_id = payload.get('sector_id') + + keywords = Keywords.objects.filter(id__in=ids, account=account).select_related( + 'account', 'site', 'sector', 'seed_keyword' + ) + + return { + 'keywords': keywords, # Keyword objects + 'keyword_data': [ # Data for AI + { + 'id': kw.id, + 'keyword': kw.keyword, # From seed_keyword relationship + 'volume': kw.volume, + 'difficulty': kw.difficulty, + 'intent': kw.intent, + } + for kw in keywords + ], + 'sector_id': sector_id + } +``` + +**Method: build_prompt()** +```python +def build_prompt(self, data: Dict, account=None) -> str: + keyword_data = data['keyword_data'] + + # Format keywords for prompt + keywords_text = '\n'.join([ + f"- {kw['keyword']} (Volume: {kw['volume']}, Difficulty: {kw['difficulty']}, Intent: {kw['intent']})" + for kw in keyword_data + ]) + + # Get prompt template from registry + prompt = PromptRegistry.get_prompt( + function_name='auto_cluster', + account=account, + context={'KEYWORDS': keywords_text} + ) + + # Ensure JSON mode compatibility + if 'json' not in prompt.lower(): + prompt += "\n\nIMPORTANT: You must respond with valid JSON only." + + return prompt +``` + +**Method: parse_response()** +```python +def parse_response(self, response: str, step_tracker=None) -> List[Dict]: + # Try direct JSON parse + try: + json_data = json.loads(response.strip()) + except json.JSONDecodeError: + # Fallback to extract_json (handles markdown code blocks) + ai_core = AICore(account=self.account) + json_data = ai_core.extract_json(response) + + # Extract clusters array + if isinstance(json_data, dict): + clusters = json_data.get('clusters', []) + elif isinstance(json_data, list): + clusters = json_data + + return clusters # [{name, keywords: [], description}] +``` + +**Method: save_output()** +```python +def save_output(self, parsed: List[Dict], original_data: Dict, account=None, + progress_tracker=None, step_tracker=None) -> Dict: + keywords = original_data['keywords'] + account = account or keywords[0].account + site = keywords[0].site + sector = keywords[0].sector + + clusters_created = 0 + keywords_updated = 0 + + with transaction.atomic(): + for cluster_data in parsed: + cluster_name = cluster_data.get('name', '') + cluster_keywords = cluster_data.get('keywords', []) + + # Get or create cluster + cluster, created = Clusters.objects.get_or_create( + name=cluster_name, + account=account, + site=site, + sector=sector, + defaults={ + 'description': cluster_data.get('description', ''), + 'status': 'active', + } + ) + + if created: + clusters_created += 1 + + # Match keywords (case-insensitive) + for keyword_obj in keywords: + if keyword_obj.keyword.lower() in [k.lower() for k in cluster_keywords]: + keyword_obj.cluster = cluster + keyword_obj.status = 'mapped' + keyword_obj.save() + keywords_updated += 1 + + # Recalculate cluster metrics + for cluster in Clusters.objects.filter(account=account, site=site, sector=sector): + cluster.keywords_count = Keywords.objects.filter(cluster=cluster).count() + cluster.volume = Keywords.objects.filter(cluster=cluster).aggregate( + total=Sum(Case( + When(volume_override__isnull=False, then=F('volume_override')), + default=F('seed_keyword__volume'), + output_field=IntegerField() + )) + )['total'] or 0 + cluster.save() + + return { + 'count': clusters_created, + 'clusters_created': clusters_created, + 'keywords_updated': keywords_updated + } +``` + +**Service Integration:** +```python +# backend/igny8_core/business/planning/services/clustering_service.py +class ClusteringService: + def cluster_keywords(self, keyword_ids, account, sector_id=None): + from igny8_core.ai.tasks import run_ai_task + + payload = {'ids': keyword_ids, 'sector_id': sector_id} + + if hasattr(run_ai_task, 'delay'): + # Async via Celery + task = run_ai_task.delay( + function_name='auto_cluster', + payload=payload, + account_id=account.id + ) + return {'success': True, 'task_id': str(task.id)} + else: + # Sync execution + result = run_ai_task( + function_name='auto_cluster', + payload=payload, + account_id=account.id + ) + return result +``` + +--- + +### 2. GenerateIdeasFunction + +**File:** `backend/igny8_core/ai/functions/generate_ideas.py` + +**Purpose:** Generate SEO-optimized content ideas from keyword clusters + +**Class Definition:** +```python +class GenerateIdeasFunction(BaseAIFunction): + def get_name(self) -> str: + return 'generate_ideas' + + def get_max_items(self) -> int: + return 10 # Max clusters per batch +``` + +**Metadata:** +```python +{ + 'display_name': 'Generate Ideas', + 'description': 'Generate SEO-optimized content ideas from keyword clusters', + 'phases': { + 'INIT': 'Initializing idea generation...', + 'PREP': 'Loading clusters...', + 'AI_CALL': 'Generating ideas with AI...', + 'PARSE': 'Parsing idea data...', + 'SAVE': 'Saving ideas...', + 'DONE': 'Ideas generated!' + } +} +``` + +**Method: prepare()** +```python +def prepare(self, payload: dict, account=None) -> Dict: + cluster_ids = payload.get('ids', []) + + clusters = Clusters.objects.filter(id__in=cluster_ids, account=account).select_related( + 'sector', 'account', 'site' + ).prefetch_related('keywords') + + cluster_data = [] + for cluster in clusters: + # Get keywords from Keywords model (via seed_keyword relationship) + keyword_objects = Keywords.objects.filter(cluster=cluster).select_related('seed_keyword') + keywords = [kw.seed_keyword.keyword for kw in keyword_objects if kw.seed_keyword] + + cluster_data.append({ + 'id': cluster.id, + 'name': cluster.name, + 'description': cluster.description or '', + 'keywords': keywords, + }) + + return { + 'clusters': clusters, + 'cluster_data': cluster_data, + 'account': account or clusters[0].account + } +``` + +**Method: build_prompt()** +```python +def build_prompt(self, data: Dict, account=None) -> str: + cluster_data = data['cluster_data'] + + clusters_text = '\n'.join([ + f"Cluster ID: {c['id']} | Name: {c['name']} | Description: {c.get('description', '')}" + for c in cluster_data + ]) + + cluster_keywords_text = '\n'.join([ + f"Cluster ID: {c['id']} | Name: {c['name']} | Keywords: {', '.join(c.get('keywords', []))}" + for c in cluster_data + ]) + + prompt = PromptRegistry.get_prompt( + function_name='generate_ideas', + account=account or data['account'], + context={ + 'CLUSTERS': clusters_text, + 'CLUSTER_KEYWORDS': cluster_keywords_text, + } + ) + + return prompt +``` + +**Method: parse_response()** +```python +def parse_response(self, response: str, step_tracker=None) -> List[Dict]: + ai_core = AICore(account=self.account) + json_data = ai_core.extract_json(response) + + if not json_data or 'ideas' not in json_data: + raise ValueError(f"Failed to parse ideas response") + + return json_data.get('ideas', []) + # Expected format: [{title, description, cluster_id, content_type, content_structure, ...}] +``` + +**Method: save_output()** +```python +def save_output(self, parsed: List[Dict], original_data: Dict, account=None, + progress_tracker=None, step_tracker=None) -> Dict: + clusters = original_data['clusters'] + account = account or original_data['account'] + + ideas_created = 0 + + with transaction.atomic(): + for idea_data in parsed: + # Match cluster by ID or name + cluster = None + cluster_id_from_ai = idea_data.get('cluster_id') + cluster_name = idea_data.get('cluster_name', '') + + if cluster_id_from_ai: + cluster = next((c for c in clusters if c.id == cluster_id_from_ai), None) + + if not cluster and cluster_name: + cluster = next((c for c in clusters if c.name == cluster_name), None) + + if not cluster: + continue + + site = cluster.site or (cluster.sector.site if cluster.sector else None) + + # Handle description (might be dict or string) + description = idea_data.get('description', '') + if isinstance(description, dict): + description = json.dumps(description) + + # Create ContentIdeas record + ContentIdeas.objects.create( + idea_title=idea_data.get('title', 'Untitled Idea'), + description=description, + content_type=idea_data.get('content_type', 'post'), + content_structure=idea_data.get('content_structure', 'article'), + target_keywords=idea_data.get('covered_keywords', '') or idea_data.get('target_keywords', ''), + keyword_cluster=cluster, + estimated_word_count=idea_data.get('estimated_word_count', 1500), + status='new', + account=account, + site=site, + sector=cluster.sector, + ) + ideas_created += 1 + + # Update cluster status + if cluster.status == 'new': + cluster.status = 'mapped' + cluster.save() + + return { + 'count': ideas_created, + 'ideas_created': ideas_created + } +``` + +**Service Integration:** +```python +# backend/igny8_core/business/planning/services/ideas_service.py +class IdeasService: + def generate_ideas(self, cluster_ids, account): + from igny8_core.ai.tasks import run_ai_task + + payload = {'ids': cluster_ids} + + if hasattr(run_ai_task, 'delay'): + task = run_ai_task.delay( + function_name='auto_generate_ideas', + payload=payload, + account_id=account.id + ) + return {'success': True, 'task_id': str(task.id)} + else: + result = run_ai_task( + function_name='auto_generate_ideas', + payload=payload, + account_id=account.id + ) + return result +``` + +--- + +## Writer Module AI Functions + +### 3. GenerateContentFunction + +**File:** `backend/igny8_core/ai/functions/generate_content.py` + +**Purpose:** Generate complete article content from task requirements + +**Class Definition:** +```python +class GenerateContentFunction(BaseAIFunction): + def get_name(self) -> str: + return 'generate_content' + + def get_max_items(self) -> int: + return 50 # Max tasks per batch +``` + +**Key Implementation Details:** + +**Method: prepare()** +```python +def prepare(self, payload: dict, account=None) -> List: + task_ids = payload.get('ids', []) + + tasks = Tasks.objects.filter(id__in=task_ids, account=account).select_related( + 'account', 'site', 'sector', 'cluster', 'taxonomy_term' + ) + + return list(tasks) +``` + +**Method: build_prompt()** +```python +def build_prompt(self, data: Any, account=None) -> str: + task = data[0] if isinstance(data, list) else data + + # Build idea data + idea_data = f"Title: {task.title or 'Untitled'}\n" + if task.description: + idea_data += f"Description: {task.description}\n" + idea_data += f"Content Type: {task.content_type or 'post'}\n" + idea_data += f"Content Structure: {task.content_structure or 'article'}\n" + + # Build cluster context + cluster_data = '' + if task.cluster: + cluster_data = f"Cluster Name: {task.cluster.name}\n" + if task.cluster.description: + cluster_data += f"Description: {task.cluster.description}\n" + + # Build taxonomy context + taxonomy_data = '' + if task.taxonomy_term: + taxonomy_data = f"Taxonomy: {task.taxonomy_term.name}\n" + if task.taxonomy_term.taxonomy_type: + taxonomy_data += f"Type: {task.taxonomy_term.get_taxonomy_type_display()}\n" + + # Build keywords + keywords_data = '' + if task.keywords: + keywords_data = f"Keywords: {task.keywords}\n" + + prompt = PromptRegistry.get_prompt( + function_name='generate_content', + account=account or task.account, + task=task, + context={ + 'IDEA': idea_data, + 'CLUSTER': cluster_data, + 'TAXONOMY': taxonomy_data, + 'KEYWORDS': keywords_data, + } + ) + + return prompt +``` + +**Method: parse_response()** +```python +def parse_response(self, response: str, step_tracker=None) -> Dict: + # Try JSON parse first + try: + parsed_json = json.loads(response.strip()) + if isinstance(parsed_json, dict): + return parsed_json + except (json.JSONDecodeError, ValueError): + pass + + # Fallback: normalize plain HTML content + try: + from igny8_core.utils.content_normalizer import normalize_content + normalized = normalize_content(response) + return {'content': normalized['normalized_content']} + except Exception: + return {'content': response} +``` + +**Method: save_output() - CRITICAL WITH TAGS/CATEGORIES** +```python +def save_output(self, parsed: Any, original_data: Any, account=None, + progress_tracker=None, step_tracker=None) -> Dict: + task = original_data[0] if isinstance(original_data, list) else original_data + + # Extract fields from parsed response + if isinstance(parsed, dict): + content_html = parsed.get('content', '') + title = parsed.get('title') or task.title + meta_title = parsed.get('meta_title') or parsed.get('seo_title') or title + meta_description = parsed.get('meta_description') or parsed.get('seo_description') + primary_keyword = parsed.get('primary_keyword') or parsed.get('focus_keyword') + secondary_keywords = parsed.get('secondary_keywords') or parsed.get('keywords', []) + tags_from_response = parsed.get('tags', []) + categories_from_response = parsed.get('categories', []) + else: + content_html = str(parsed) + title = task.title + # ... defaults + tags_from_response = [] + categories_from_response = [] + + # Calculate word count + word_count = 0 + if content_html: + text_for_counting = re.sub(r'<[^>]+>', '', content_html) + word_count = len(text_for_counting.split()) + + # Create Content record (independent, NOT OneToOne with Task) + content_record = Content.objects.create( + title=title, + content_html=content_html, + word_count=word_count, + meta_title=meta_title, + meta_description=meta_description, + primary_keyword=primary_keyword, + secondary_keywords=secondary_keywords if isinstance(secondary_keywords, list) else [], + cluster=task.cluster, + content_type=task.content_type, + content_structure=task.content_structure, + source='igny8', + status='draft', + account=task.account, + site=task.site, + sector=task.sector, + ) + + # Link taxonomy term from task + if task.taxonomy_term: + content_record.taxonomy_terms.add(task.taxonomy_term) + + # Process tags from AI response + if tags_from_response and isinstance(tags_from_response, list): + from django.utils.text import slugify + for tag_name in tags_from_response: + if tag_name and isinstance(tag_name, str): + tag_name = tag_name.strip() + if tag_name: + tag_slug = slugify(tag_name) + tag_obj, created = ContentTaxonomy.objects.get_or_create( + site=task.site, + slug=tag_slug, + taxonomy_type='tag', + defaults={ + 'name': tag_name, + 'sector': task.sector, + 'account': task.account, + } + ) + content_record.taxonomy_terms.add(tag_obj) + + # Process categories from AI response + if categories_from_response and isinstance(categories_from_response, list): + from django.utils.text import slugify + for category_name in categories_from_response: + if category_name and isinstance(category_name, str): + category_name = category_name.strip() + if category_name: + category_slug = slugify(category_name) + category_obj, created = ContentTaxonomy.objects.get_or_create( + site=task.site, + slug=category_slug, + taxonomy_type='category', + defaults={ + 'name': category_name, + 'sector': task.sector, + 'account': task.account, + } + ) + content_record.taxonomy_terms.add(category_obj) + + # Update task status + task.status = 'completed' + task.save(update_fields=['status', 'updated_at']) + + # Auto-sync idea status + if hasattr(task, 'idea') and task.idea: + task.idea.status = 'completed' + task.idea.save(update_fields=['status', 'updated_at']) + + return { + 'count': 1, + 'content_id': content_record.id, + 'task_id': task.id, + 'word_count': word_count, + } +``` + +--- + +### 4. GenerateImagePromptsFunction + +**File:** `backend/igny8_core/ai/functions/generate_image_prompts.py` + +**Purpose:** Extract detailed image generation prompts from content HTML + +**Class Definition:** +```python +class GenerateImagePromptsFunction(BaseAIFunction): + def get_name(self) -> str: + return 'generate_image_prompts' + + def get_max_items(self) -> int: + return 50 # Max content records per batch +``` + +**Method: prepare()** +```python +def prepare(self, payload: dict, account=None) -> List: + content_ids = payload.get('ids', []) + + contents = Content.objects.filter(id__in=content_ids, account=account).select_related( + 'account', 'site', 'sector', 'cluster' + ) + + max_images = self._get_max_in_article_images(account) + + extracted_data = [] + for content in contents: + extracted = self._extract_content_elements(content, max_images) + extracted_data.append({ + 'content': content, + 'extracted': extracted, + 'max_images': max_images, + }) + + return extracted_data +``` + +**Helper: _extract_content_elements()** +```python +def _extract_content_elements(self, content: Content, max_images: int) -> Dict: + from bs4 import BeautifulSoup + + html_content = content.content_html or '' + soup = BeautifulSoup(html_content, 'html.parser') + + # Extract title + title = content.title or '' + + # Extract intro paragraphs (skip italic hook) + paragraphs = soup.find_all('p') + intro_paragraphs = [] + for p in paragraphs[:3]: + text = p.get_text(strip=True) + if len(text.split()) > 50: # Real paragraph + intro_paragraphs.append(text) + if len(intro_paragraphs) >= 2: + break + + # Extract H2 headings + h2_tags = soup.find_all('h2') + h2_headings = [h2.get_text(strip=True) for h2 in h2_tags[:max_images]] + + return { + 'title': title, + 'intro_paragraphs': intro_paragraphs, + 'h2_headings': h2_headings, + } +``` + +**Method: save_output()** +```python +def save_output(self, parsed: Dict, original_data: Any, account=None, + progress_tracker=None, step_tracker=None) -> Dict: + data = original_data[0] if isinstance(original_data, list) else original_data + content = data['content'] + max_images = data['max_images'] + + prompts_created = 0 + + with transaction.atomic(): + # Save featured image prompt + Images.objects.update_or_create( + content=content, + image_type='featured', + defaults={ + 'prompt': parsed['featured_prompt'], + 'status': 'pending', + 'position': 0, + } + ) + prompts_created += 1 + + # Save in-article image prompts + in_article_prompts = parsed.get('in_article_prompts', []) + for idx, prompt_text in enumerate(in_article_prompts[:max_images]): + Images.objects.update_or_create( + content=content, + image_type='in_article', + position=idx + 1, + defaults={ + 'prompt': prompt_text, + 'status': 'pending', + } + ) + prompts_created += 1 + + return { + 'count': prompts_created, + 'prompts_created': prompts_created, + } +``` + +--- + +### 5. GenerateImagesFunction + +**File:** `backend/igny8_core/ai/functions/generate_images.py` + +**Purpose:** Generate actual image URLs from Image records with prompts + +**Note:** This function is partially implemented. The actual image generation happens via provider APIs. + +--- + +## AI Function Base Class + +**File:** `backend/igny8_core/ai/base.py` + +All AI functions inherit from this abstract base: + +```python +class BaseAIFunction(ABC): + """Base class for all AI functions""" + + @abstractmethod + def get_name(self) -> str: + """Return function name (e.g., 'auto_cluster')""" + pass + + def get_metadata(self) -> Dict: + """Return function metadata (display name, description, phases)""" + return { + 'display_name': self.get_name().replace('_', ' ').title(), + 'description': f'{self.get_name()} AI function', + 'phases': { + 'INIT': 'Initializing...', + 'PREP': 'Preparing data...', + 'AI_CALL': 'Processing with AI...', + 'PARSE': 'Parsing response...', + 'SAVE': 'Saving results...', + 'DONE': 'Complete!' + } + } + + def validate(self, payload: dict, account=None) -> Dict[str, Any]: + """Validate input payload""" + ids = payload.get('ids', []) + if not ids: + return {'valid': False, 'error': 'No IDs provided'} + return {'valid': True} + + def get_max_items(self) -> Optional[int]: + """Override to set max items limit""" + return None + + @abstractmethod + def prepare(self, payload: dict, account=None) -> Any: + """Load and prepare data for AI processing""" + pass + + @abstractmethod + def build_prompt(self, data: Any, account=None) -> str: + """Build AI prompt from prepared data""" + pass + + def get_model(self, account=None) -> Optional[str]: + """Override to specify model (defaults to account's default model)""" + return None + + @abstractmethod + def parse_response(self, response: str, step_tracker=None) -> Any: + """Parse AI response into structured data""" + pass + + @abstractmethod + def save_output(self, parsed: Any, original_data: Any, account=None, + progress_tracker=None, step_tracker=None) -> Dict: + """Save parsed results to database""" + pass +``` + +--- + +## AI Engine & Execution + +**File:** `backend/igny8_core/ai/engine.py` + +The `AIEngine` orchestrates all AI function execution: + +```python +class AIEngine: + def __init__(self, account: Account): + self.account = account + self.ai_core = AICore(account=account) + + def execute(self, fn: BaseAIFunction, payload: dict) -> Dict: + """ + Execute AI function with full orchestration: + 1. Validation + 2. Preparation + 3. AI call + 4. Response parsing + 5. Output saving + 6. Credit deduction (automatic) + 7. Progress tracking + 8. Logging + """ + + # Step 1: Validate + validation = fn.validate(payload, self.account) + if not validation['valid']: + return {'success': False, 'error': validation['error']} + + # Step 2: Prepare data + prepared_data = fn.prepare(payload, self.account) + + # Step 3: Build prompt + prompt = fn.build_prompt(prepared_data, self.account) + + # Step 4: Call AI (via AICore) + model = fn.get_model(self.account) or self._get_default_model() + response = self.ai_core.run_ai_request( + prompt=prompt, + model=model, + function_name=fn.get_name() + ) + + # Step 5: Parse response + parsed = fn.parse_response(response['content']) + + # Step 6: Save output + result = fn.save_output(parsed, prepared_data, self.account) + + # Step 7: Deduct credits (AUTOMATIC - line 395) + CreditService.deduct_credits_for_operation( + account=self.account, + operation_type=self._get_operation_type(), + amount=self._get_actual_amount(), + ) + + # Step 8: Log to AIUsageLog + AIUsageLog.objects.create( + account=self.account, + function_name=fn.get_name(), + credits_used=credits_deducted, + # ... other fields + ) + + return { + 'success': True, + **result + } +``` + +**Key Point:** Credits are AUTOMATICALLY deducted by AIEngine. AI functions do NOT handle credits themselves. + +--- + +## Credit System Integration + +**Automatic Credit Deduction:** + +All credit management happens in `AIEngine.execute()` at line 395: + +```python +# backend/igny8_core/ai/engine.py line 395 +CreditService.deduct_credits_for_operation( + account=account, + operation_type=self._get_operation_type(), + amount=self._get_actual_amount(), +) +``` + +**AI Functions DO NOT:** +- Calculate credit costs +- Call `CreditService` manually +- Handle credit errors (handled by AIEngine) + +**AI Functions ONLY:** +- Focus on their specific logic +- Return `{'count': N}` in `save_output()` +- AIEngine uses `count` to calculate credits + +--- + +## Progress Tracking + +**StepTracker & ProgressTracker:** + +All AI functions emit progress events through trackers: + +```python +# Phases emitted automatically by AIEngine +phases = { + 'INIT': 'Initializing...', # 0-10% + 'PREP': 'Preparing data...', # 10-20% + 'AI_CALL': 'Processing with AI...', # 20-80% + 'PARSE': 'Parsing response...', # 80-90% + 'SAVE': 'Saving results...', # 90-100% + 'DONE': 'Complete!' # 100% +} +``` + +Frontend can listen to these events via: +- Celery task status polling +- WebSocket connections +- REST API `/task-progress/:task_id/` endpoint + +--- + +## Summary + +**6 AI Functions - All Production Ready:** + +| Function | Lines of Code | Status | Used By | +|----------|---------------|--------|---------| +| `auto_cluster` | ~380 | ✅ Complete | Planner, Automation Stage 1 | +| `generate_ideas` | ~250 | ✅ Complete | Planner, Automation Stage 2 | +| `generate_content` | ~400 | ✅ Complete | Writer, Automation Stage 4 | +| `generate_image_prompts` | ~280 | ✅ Complete | Writer, Automation Stage 5 | +| `generate_images` | ~300 | ⚠️ Partial | Writer, Automation Stage 6 | +| `optimize_content` | ~200 | ✅ Complete | Writer (Manual) | + +**Architecture Benefits:** +- Single source of truth for AI operations +- Consistent credit management +- Unified error handling +- Centralized progress tracking +- Easy to add new AI functions (inherit from BaseAIFunction) + +### Current gaps vs code (Dec 2025) +- AIEngine now performs a credit pre-check before the AI call (still deducts after SAVE); this is not reflected in earlier notes. +- `generate_images` implementation is partially broken: it expects task IDs (not image IDs), tries to read `task.content` (field does not exist), and uses the `extract_image_prompts` prompt path; credit estimation also looks for `image_ids`. Treat it as partial/needs fix. +- AIEngine includes messaging/cost maps for `generate_site_structure` (extra function beyond the documented six); not presently documented above. + +--- + +## Automation Integration + +**VERIFIED:** All AI functions are integrated into the IGNY8 Automation Pipeline. + +### 7-Stage Automation Pipeline + +The automation system (`backend/igny8_core/business/automation/services/automation_service.py`) uses 5 of the 6 AI functions: + +``` +Stage 1: Keywords → Clusters + ↓ Uses: AutoClusterFunction + ↓ Credits: ~0.2 per keyword + +Stage 2: Clusters → Ideas + ↓ Uses: GenerateIdeasFunction + ↓ Credits: 2 per cluster + +Stage 3: Ideas → Tasks + ↓ Uses: None (Local operation) + ↓ Credits: 0 + +Stage 4: Tasks → Content + ↓ Uses: GenerateContentFunction + ↓ Credits: ~5 per task (2500 words) + +Stage 5: Content → Image Prompts + ↓ Uses: GenerateImagePromptsFunction + ↓ Credits: ~2 per content + +Stage 6: Image Prompts → Images + ↓ Uses: GenerateImagesFunction ⚠️ + ↓ Credits: 1-4 per image + +Stage 7: Manual Review Gate + ↓ Uses: None (Manual intervention) + ↓ Credits: 0 +``` + +### Automation Execution Flow + +```python +# AutomationService.run_stage_1() example +def run_stage_1(self): + keywords = Keywords.objects.filter(site=self.site, status='new')[:batch_size] + + # Call AI function via Celery + from igny8_core.ai.tasks import run_ai_task + result = run_ai_task( + function_name='auto_cluster', + payload={'ids': [k.id for k in keywords]}, + account_id=self.account.id + ) + + # Credits automatically deducted by AIEngine + return { + 'keywords_processed': len(keywords), + 'clusters_created': result.get('count', 0), + 'credits_used': result.get('credits_used', 0) + } +``` + +### Frontend Access + +**Automation Page:** `/automation` (Fully functional) +- Real-time pipeline overview +- Manual trigger ("Run Now" button) +- Pause/Resume controls +- Live progress tracking +- Activity logs +- Run history + +**Planner & Writer Pages:** Individual AI function triggers +- Cluster keywords (Stage 1) +- Generate ideas (Stage 2) +- Generate content (Stage 4) +- Extract image prompts (Stage 5) +- Generate images (Stage 6) + +--- + +**End of AI Functions Reference** diff --git a/docs/automation/AUTOMATION-REFERENCE.md b/old-docs/automation/AUTOMATION-REFERENCE.md similarity index 100% rename from docs/automation/AUTOMATION-REFERENCE.md rename to old-docs/automation/AUTOMATION-REFERENCE.md diff --git a/docs/backend/IGNY8-BACKEND-ARCHITECTURE.md b/old-docs/backend/IGNY8-BACKEND-ARCHITECTURE.md similarity index 100% rename from docs/backend/IGNY8-BACKEND-ARCHITECTURE.md rename to old-docs/backend/IGNY8-BACKEND-ARCHITECTURE.md diff --git a/docs/backend/IGNY8-PLANNER-BACKEND.md b/old-docs/backend/IGNY8-PLANNER-BACKEND.md similarity index 100% rename from docs/backend/IGNY8-PLANNER-BACKEND.md rename to old-docs/backend/IGNY8-PLANNER-BACKEND.md diff --git a/docs/backend/IGNY8-WRITER-BACKEND.md b/old-docs/backend/IGNY8-WRITER-BACKEND.md similarity index 100% rename from docs/backend/IGNY8-WRITER-BACKEND.md rename to old-docs/backend/IGNY8-WRITER-BACKEND.md diff --git a/docs/billing/billing-account-final-plan-2025-12-05.md b/old-docs/billing/billing-account-final-plan-2025-12-05.md similarity index 100% rename from docs/billing/billing-account-final-plan-2025-12-05.md rename to old-docs/billing/billing-account-final-plan-2025-12-05.md diff --git a/docs/billing/credits-system-audit-and-improvement-plan.md b/old-docs/billing/credits-system-audit-and-improvement-plan.md similarity index 100% rename from docs/billing/credits-system-audit-and-improvement-plan.md rename to old-docs/billing/credits-system-audit-and-improvement-plan.md diff --git a/old-docs/igny8-app/02-PLANNER-WRITER-WORKFLOW-TECHNICAL-GUIDE.md b/old-docs/igny8-app/02-PLANNER-WRITER-WORKFLOW-TECHNICAL-GUIDE.md new file mode 100644 index 00000000..2bc613a7 --- /dev/null +++ b/old-docs/igny8-app/02-PLANNER-WRITER-WORKFLOW-TECHNICAL-GUIDE.md @@ -0,0 +1,1467 @@ +# IGNY8 Planner → Writer Workflow Documentation +**Complete Technical Implementation Guide** +**Date:** November 24, 2025 +**Version:** 1.0 + +--- + + +## Table of Contents +1. [Overview](#overview) +2. [Database Tables & Models](#database-tables--models) +3. [Frontend Pages & Components](#frontend-pages--components) +4. [Backend API Endpoints](#backend-api-endpoints) +5. [Complete Workflow](#complete-workflow) +6. [AI Functions](#ai-functions) +7. [Flowchart Diagrams](#flowchart-diagrams) + +--- + +## Overview + +The IGNY8 content workflow consists of two primary modules: +- **Planner Module**: Keyword research, clustering, and content ideation +- **Writer Module**: Task management, content generation, and publishing + +Data flows sequentially through these stages: +``` +Keywords → Clusters → Ideas → Tasks → Content → Images +``` + +--- + +## Database Tables & Models + +### Planner Module Tables + +#### 1. `igny8_keywords` (Keywords Model) +**Location:** `backend/igny8_core/business/planning/models.py` + +| Field | Type | Description | Choices | +|-------|------|-------------|---------| +| `id` | Integer | Primary key | - | +| `seed_keyword_id` | ForeignKey | Reference to global SeedKeyword | - | +| `volume_override` | Integer | Site-specific volume override | - | +| `difficulty_override` | Integer | Site-specific difficulty override | - | +| `cluster_id` | ForeignKey | Parent cluster | Clusters | +| `status` | CharField(50) | Keyword status | active, pending, archived | +| `account` | ForeignKey | Owner account | - | +| `site` | ForeignKey | Parent site | - | +| `sector` | ForeignKey | Parent sector | - | +| `created_at` | DateTime | Creation timestamp | - | +| `updated_at` | DateTime | Last update timestamp | - | + +**Indexes:** +- `seed_keyword` +- `status` +- `cluster` +- `site, sector` +- `seed_keyword, site, sector` (unique) + +**Properties (Computed from SeedKeyword):** +- `keyword` - Text from seed_keyword.keyword +- `volume` - volume_override or seed_keyword.volume +- `difficulty` - difficulty_override or seed_keyword.difficulty +- `intent` - seed_keyword.intent + +--- + +#### 2. `igny8_clusters` (Clusters Model) +**Location:** `backend/igny8_core/business/planning/models.py` + +| Field | Type | Description | Choices | +|-------|------|-------------|---------| +| `id` | Integer | Primary key | - | +| `name` | CharField(255) | Cluster name (unique) | - | +| `description` | TextField | Cluster description | - | +| `keywords_count` | Integer | Number of keywords | - | +| `volume` | Integer | Total search volume | - | +| `mapped_pages` | Integer | Number of mapped pages | - | +| `status` | CharField(50) | Cluster status | active, archived | +| `context_type` | CharField(50) | Cluster dimension | topic, attribute, service_line | +| `dimension_meta` | JSONField | Extended metadata | - | +| `account` | ForeignKey | Owner account | - | +| `site` | ForeignKey | Parent site | - | +| `sector` | ForeignKey | Parent sector | - | +| `created_at` | DateTime | Creation timestamp | - | +| `updated_at` | DateTime | Last update timestamp | - | + +**Indexes:** +- `name` +- `status` +- `site, sector` +- `context_type` + +**Relationships:** +- `keywords` (1:M) - Related Keywords +- `ideas` (1:M) - Related ContentIdeas +- `tasks` (1:M) - Related Tasks +- `contents` (1:M) - Related Content + +--- + +#### 3. `igny8_content_ideas` (ContentIdeas Model) +**Location:** `backend/igny8_core/business/planning/models.py` + +| Field | Type | Description | Choices | +|-------|------|-------------|---------| +| `id` | Integer | Primary key | - | +| `idea_title` | CharField(255) | Idea title | - | +| `description` | TextField | Idea description | - | +| `target_keywords` | CharField(500) | Comma-separated keywords (legacy) | - | +| `keyword_objects` | ManyToManyField | Keywords linked to idea | Keywords | +| `keyword_cluster_id` | ForeignKey | Parent cluster | Clusters | +| `taxonomy_id` | ForeignKey | Optional taxonomy association | ContentTaxonomy | +| `status` | CharField(50) | Idea status | new, scheduled, published | +| `estimated_word_count` | Integer | Target word count | - | +| `site_entity_type` | CharField(50) | Target entity type | post, page, product, service, taxonomy_term | +| `cluster_role` | CharField(50) | Role within cluster | hub, supporting, attribute | +| `account` | ForeignKey | Owner account | - | +| `site` | ForeignKey | Parent site | - | +| `sector` | ForeignKey | Parent sector | - | +| `created_at` | DateTime | Creation timestamp | - | +| `updated_at` | DateTime | Last update timestamp | - | + +**Indexes:** +- `idea_title` +- `status` +- `keyword_cluster` +- `site_entity_type` +- `cluster_role` +- `site, sector` + +--- + +### Writer Module Tables + +#### 4. `igny8_tasks` (Tasks Model) +**Location:** `backend/igny8_core/business/content/models.py` + +| Field | Type | Description | Choices | +|-------|------|-------------|---------| +| `id` | Integer | Primary key | - | +| `title` | CharField(255) | Task title | - | +| `description` | TextField | Task description | - | +| `keywords` | CharField(500) | Comma-separated keywords (legacy) | - | +| `cluster_id` | ForeignKey | Parent cluster | Clusters | +| `keyword_objects` | ManyToManyField | Keywords linked to task | Keywords | +| `idea_id` | ForeignKey | Source idea | ContentIdeas | +| `status` | CharField(50) | Task status | queued, in_progress, completed, failed | +| `entity_type` | CharField(50) | Content entity type | post, page, product, service, taxonomy_term | +| `taxonomy_id` | ForeignKey | Taxonomy association | ContentTaxonomy | +| `cluster_role` | CharField(50) | Role within cluster | hub, supporting, attribute | +| `account` | ForeignKey | Owner account | - | +| `site` | ForeignKey | Parent site | - | +| `sector` | ForeignKey | Parent sector | - | +| `created_at` | DateTime | Creation timestamp | - | +| `updated_at` | DateTime | Last update timestamp | - | + +**Indexes:** +- `title` +- `status` +- `cluster` +- `entity_type` +- `cluster_role` +- `site, sector` + +**Relationships:** +- `content_record` (1:1) - Related Content (OneToOneField) + +--- + +#### 5. `igny8_content` (Content Model) +**Location:** `backend/igny8_core/business/content/models.py` + +| Field | Type | Description | Choices | +|-------|------|-------------|---------| +| `id` | Integer | Primary key | - | +| `task_id` | OneToOneField | Parent task | Tasks | +| `html_content` | TextField | Final HTML content | - | +| `word_count` | Integer | Content word count | - | +| `metadata` | JSONField | Additional metadata | - | +| `title` | CharField(255) | Content title | - | +| `meta_title` | CharField(255) | SEO meta title | - | +| `meta_description` | TextField | SEO meta description | - | +| `primary_keyword` | CharField(255) | Primary keyword | - | +| `secondary_keywords` | JSONField | List of secondary keywords | - | +| `status` | CharField(50) | Content workflow status | draft, review, publish | +| `source` | CharField(50) | Content source | igny8, wordpress, shopify, custom | +| `sync_status` | CharField(50) | Sync status | native, imported, synced | +| `external_id` | CharField(255) | External platform ID | - | +| `external_url` | URLField | External platform URL | - | +| `external_type` | CharField(100) | External post type | - | +| `sync_metadata` | JSONField | Platform-specific sync metadata | - | +| `internal_links` | JSONField | Internal links (linker) | - | +| `linker_version` | Integer | Linker processing version | - | +| `optimizer_version` | Integer | Optimizer processing version | - | +| `optimization_scores` | JSONField | Optimization scores | - | +| `entity_type` | CharField(50) | Content entity type | post, page, product, service, taxonomy_term | +| `content_format` | CharField(50) | Content format (posts only) | article, listicle, guide, comparison, review, roundup | +| `cluster_role` | CharField(50) | Role within cluster | hub, supporting, attribute | +| `json_blocks` | JSONField | Structured content blocks | - | +| `structure_data` | JSONField | Content structure data | - | +| `taxonomies` | ManyToManyField | Associated taxonomy terms | ContentTaxonomy | +| `cluster_id` | ForeignKey | Primary semantic cluster | Clusters | +| `account` | ForeignKey | Owner account | - | +| `site` | ForeignKey | Parent site | - | +| `sector` | ForeignKey | Parent sector | - | +| `generated_at` | DateTime | Generation timestamp | - | +| `updated_at` | DateTime | Last update timestamp | - | + +**Indexes:** +- `task` +- `generated_at` +- `source` +- `sync_status` +- `source, sync_status` +- `entity_type` +- `content_format` +- `cluster_role` +- `cluster` +- `external_type` +- `site, entity_type` + +--- + +#### 6. `igny8_images` (Images Model) +**Location:** `backend/igny8_core/business/content/models.py` + +| Field | Type | Description | Choices | +|-------|------|-------------|---------| +| `id` | Integer | Primary key | - | +| `content_id` | ForeignKey | Parent content | Content | +| `image_type` | CharField(50) | Image type | featured, in_article | +| `position` | Integer | Position in article | - | +| `prompt` | TextField | Image generation prompt | - | +| `image_url` | URLField | Generated image URL | - | +| `status` | CharField(50) | Image status | pending, generated, failed | +| `provider` | CharField(50) | Image generation provider | dall-e, stable-diffusion, midjourney | +| `model` | CharField(100) | Image generation model | - | +| `error_message` | TextField | Error message (if failed) | - | +| `metadata` | JSONField | Additional metadata | - | +| `account` | ForeignKey | Owner account | - | +| `site` | ForeignKey | Parent site | - | +| `sector` | ForeignKey | Parent sector | - | +| `created_at` | DateTime | Creation timestamp | - | +| `updated_at` | DateTime | Last update timestamp | - | + +**Indexes:** +- `content` +- `image_type` +- `status` +- `site, sector` + +--- + +## Frontend Pages & Components + +### Planner Module Pages + +#### 1. Keywords Page (`/planner/keywords`) +**Component:** `frontend/src/pages/Planner/Keywords.tsx` + +**Key Functions:** +- `loadKeywords()` - Fetch keywords from API with filters/pagination +- `handleCreateSeedKeyword()` - Attach SeedKeyword to site/sector +- `handleEdit(keyword)` - Edit keyword (override volume/difficulty, assign cluster) +- `handleBulkAction('auto_cluster', ids)` - AI clustering (max 20 keywords) +- `handleBulkDelete(ids)` - Delete keywords +- `handleBulkUpdateStatus(ids, status)` - Update status +- `handleExport()` - Export keywords to CSV +- `handleImportClick()` - Import keywords from CSV + +**API Calls:** +```typescript +fetchKeywords(filters: KeywordFilters): Promise> +createKeyword(data: KeywordCreateData): Promise +updateKeyword(id: number, data: KeywordCreateData): Promise +deleteKeyword(id: number): Promise +bulkDeleteKeywords(ids: number[]): Promise<{ deleted_count: number }> +bulkUpdateKeywordsStatus(ids: number[], status: string): Promise<{ updated_count: number }> +autoClusterKeywords(ids: number[], sectorId?: number): Promise +``` + +**State Management:** +- `keywords` - Keyword list +- `clusters` - Cluster dropdown options +- `availableSeedKeywords` - Available SeedKeywords for attachment +- `searchTerm`, `statusFilter`, `clusterFilter`, `intentFilter`, `difficultyFilter`, `volumeMin`, `volumeMax` - Filters +- `currentPage`, `totalPages`, `totalCount` - Pagination +- `sortBy`, `sortDirection` - Sorting +- `selectedIds` - Bulk selection + +**AI Function Logs:** +When Resource Debug is enabled, logs all AI function calls: +- Request: keyword_ids, sector_id +- Success: task_id (async) or clusters_created, keywords_updated (sync) +- Progress steps: phase, percentage, message +- Errors: error message + +--- + +#### 2. Clusters Page (`/planner/clusters`) +**Component:** `frontend/src/pages/Planner/Clusters.tsx` + +**Key Functions:** +- `loadClusters()` - Fetch clusters from API +- `handleCreateCluster()` - Create new cluster manually +- `handleEdit(cluster)` - Edit cluster name/description +- `handleRowAction('generate_ideas', cluster)` - Generate ideas for single cluster +- `handleBulkAction('auto_generate_ideas', ids)` - Generate ideas for multiple clusters (max 5) +- `handleBulkDelete(ids)` - Delete clusters +- `handleBulkUpdateStatus(ids, status)` - Update status + +**API Calls:** +```typescript +fetchClusters(filters: ClusterFilters): Promise> +createCluster(data: ClusterCreateData): Promise +updateCluster(id: number, data: ClusterCreateData): Promise +deleteCluster(id: number): Promise +bulkDeleteClusters(ids: number[]): Promise<{ deleted_count: number }> +bulkUpdateClustersStatus(ids: number[], status: string): Promise<{ updated_count: number }> +autoGenerateIdeas(clusterIds: number[]): Promise +``` + +**State Management:** +- `clusters` - Cluster list +- `searchTerm`, `statusFilter`, `difficultyFilter`, `volumeMin`, `volumeMax` - Filters +- `currentPage`, `totalPages`, `totalCount` - Pagination +- `sortBy`, `sortDirection` - Sorting +- `selectedIds` - Bulk selection + +**AI Progress Modal:** +Shows progress for `auto_generate_ideas` async tasks: +- Title: "Generating Content Ideas" +- Function ID: `ai-generate-ideas-01-desktop` +- Displays: phase, percentage, message, step logs + +--- + +#### 3. Ideas Page (`/planner/ideas`) +**Component:** `frontend/src/pages/Planner/Ideas.tsx` + +**Key Functions:** +- `loadIdeas()` - Fetch content ideas from API +- `handleCreateIdea()` - Create new idea manually +- `handleEdit(idea)` - Edit idea title/description/cluster +- `handleRowAction('queue_to_writer', idea)` - Convert single idea to task +- `handleBulkAction('queue_to_writer', ids)` - Convert multiple ideas to tasks +- `handleBulkDelete(ids)` - Delete ideas +- `handleBulkUpdateStatus(ids, status)` - Update status + +**API Calls:** +```typescript +fetchContentIdeas(filters: ContentIdeasFilters): Promise> +createContentIdea(data: ContentIdeaCreateData): Promise +updateContentIdea(id: number, data: ContentIdeaCreateData): Promise +deleteContentIdea(id: number): Promise +bulkDeleteContentIdeas(ids: number[]): Promise<{ deleted_count: number }> +bulkUpdateContentIdeasStatus(ids: number[], status: string): Promise<{ updated_count: number }> +bulkQueueIdeasToWriter(ids: number[]): Promise<{ created_count: number }> +``` + +**State Management:** +- `ideas` - Idea list +- `clusters` - Cluster dropdown options +- `searchTerm`, `statusFilter`, `clusterFilter`, `structureFilter`, `typeFilter`, `entityTypeFilter` - Filters +- `currentPage`, `totalPages`, `totalCount` - Pagination +- `sortBy`, `sortDirection` - Sorting +- `selectedIds` - Bulk selection + +**Queue to Writer Logic:** +Only ideas with `status='new'` can be queued. Creates Tasks with: +- `title` = `idea_title` +- `description` = `idea.description` +- `keywords` = `idea.target_keywords` +- `cluster` = `idea.keyword_cluster` +- `idea` = `idea.id` +- `status` = 'queued' +- `entity_type` = `idea.site_entity_type` +- `cluster_role` = `idea.cluster_role` +- `taxonomy` = `idea.taxonomy` + +--- + +### Writer Module Pages + +#### 4. Tasks Page (`/writer/tasks`) +**Component:** `frontend/src/pages/Writer/Tasks.tsx` + +**Key Functions:** +- `loadTasks()` - Fetch tasks from API +- `handleCreateTask()` - Create new task manually +- `handleEdit(task)` - Edit task title/description/cluster +- `handleRowAction('generate_content', task)` - Generate content for single task (AI) +- `handleBulkAction('generate_images', ids)` - Generate images for multiple tasks (max 10) +- `handleBulkDelete(ids)` - Delete tasks +- `handleBulkUpdateStatus(ids, status)` - Update status + +**API Calls:** +```typescript +fetchTasks(filters: TasksFilters): Promise> +createTask(data: TaskCreateData): Promise +updateTask(id: number, data: TaskCreateData): Promise +deleteTask(id: number): Promise +bulkDeleteTasks(ids: number[]): Promise<{ deleted_count: number }> +bulkUpdateTasksStatus(ids: number[], status: string): Promise<{ updated_count: number }> +autoGenerateContent(taskIds: number[]): Promise +autoGenerateImages(taskIds: number[]): Promise +``` + +**State Management:** +- `tasks` - Task list +- `clusters` - Cluster dropdown options +- `searchTerm`, `statusFilter`, `clusterFilter`, `structureFilter`, `typeFilter`, `sourceFilter`, `entityTypeFilter` - Filters +- `currentPage`, `totalPages`, `totalCount` - Pagination +- `sortBy`, `sortDirection` - Sorting +- `selectedIds` - Bulk selection +- `aiLogs` - AI function logs (when Resource Debug enabled) + +**AI Function Logs:** +Logs all AI function calls: +- Request: task_ids, task_title +- Success: task_id (async) or tasks_updated, images_created (sync) +- Progress steps: phase, percentage, message +- Errors: error message + +**AI Progress Modal:** +Shows progress for `auto_generate_content` async tasks: +- Title: "Generating Content" +- Function ID: `ai-generate-content-03` +- Displays: phase, percentage, message, step logs + +--- + +#### 5. Content Page (`/writer/content`) +**Component:** `frontend/src/pages/Writer/Content.tsx` + +**Key Functions:** +- `loadContent()` - Fetch content from API +- `handleRowAction('generate_image_prompts', content)` - Generate AI image prompts +- `handleRowAction('optimize', content)` - Run optimizer on content +- `handleRowAction('send_to_optimizer', content)` - Navigate to optimizer page + +**API Calls:** +```typescript +fetchContent(filters: ContentFilters): Promise> +generateImagePrompts(contentIds: number[]): Promise +optimizerApi.optimize(contentId: number, source: string): Promise +``` + +**State Management:** +- `content` - Content list +- `searchTerm`, `statusFilter`, `sourceFilter`, `syncStatusFilter` - Filters +- `currentPage`, `totalPages`, `totalCount` - Pagination +- `sortBy`, `sortDirection` - Sorting +- `selectedIds` - Bulk selection + +**Content Row Actions:** +1. **View** - Navigate to `/writer/content/:id` (ContentView page) +2. **Generate Image Prompts** - AI function to create smart image prompts +3. **Optimize** - Run optimizer to improve SEO scores +4. **Send to Optimizer** - Navigate to `/optimizer/content?contentId=:id` + +--- + +#### 6. Images Page (`/writer/images`) +**Component:** `frontend/src/pages/Writer/Images.tsx` + +**Key Functions:** +- `loadImages()` - Fetch content images grouped by content +- `handleRowAction('update_status', imageGroup)` - Update all images for content +- `handleGenerateImages(contentId)` - Generate images using AI (opens ImageQueueModal) +- `handleBulkExport(ids)` - Export image metadata + +**API Calls:** +```typescript +fetchContentImages(filters: {}): Promise +fetchImageGenerationSettings(): Promise +generateImages(contentId: number, imageType: string, prompt: string, provider: string, model: string): Promise<{ image_url: string }> +bulkUpdateImagesStatus(contentId: number, status: string): Promise<{ updated_count: number }> +``` + +**State Management:** +- `images` - ContentImagesGroup list (one row per content) +- `searchTerm`, `statusFilter` - Filters +- `currentPage`, `totalPages`, `totalCount` - Client-side pagination +- `sortBy`, `sortDirection` - Client-side sorting +- `selectedIds` - Bulk selection +- `imageQueue` - Image generation queue items +- `taskId`, `imageModel`, `imageProvider` - Image generation settings + +**ImageQueueModal:** +Displays real-time image generation progress: +- Featured image (always first) +- In-article images (up to max_in_article_images from settings) +- Shows: index, label, type, prompt, status, progress, image preview +- Updates via polling or WebSocket + +**Content Images Group Structure:** +```typescript +{ + content_id: number; + content_title: string; + featured_image: { id, prompt, image_url, status, provider, model }; + in_article_images: [{ id, position, prompt, image_url, status, provider, model }]; + overall_status: 'pending' | 'partial' | 'complete' | 'failed'; + total_images: number; + generated_images: number; +} +``` + +--- + +## Backend API Endpoints + +### Planner Module Endpoints + +#### Keywords API +**Base URL:** `/v1/planner/keywords/` + +| Endpoint | Method | Function | Description | +|----------|--------|----------|-------------| +| `/` | GET | `list()` | List keywords with filters/pagination | +| `/` | POST | `create()` | Create new keyword (attach SeedKeyword) | +| `/:id/` | GET | `retrieve()` | Get single keyword details | +| `/:id/` | PUT/PATCH | `update()` | Update keyword | +| `/:id/` | DELETE | `destroy()` | Delete keyword | +| `/bulk_delete/` | POST | `bulk_delete()` | Delete multiple keywords | +| `/bulk_update_status/` | POST | `bulk_update_status()` | Update status for multiple keywords | +| `/auto_cluster/` | POST | `auto_cluster()` | AI clustering function | +| `/export/` | GET | `export()` | Export keywords to CSV | +| `/import/` | POST | `import()` | Import keywords from CSV | + +**ViewSet:** `KeywordsViewSet` in `backend/igny8_core/modules/planner/views.py` + +--- + +#### Clusters API +**Base URL:** `/v1/planner/clusters/` + +| Endpoint | Method | Function | Description | +|----------|--------|----------|-------------| +| `/` | GET | `list()` | List clusters with filters/pagination | +| `/` | POST | `create()` | Create new cluster | +| `/:id/` | GET | `retrieve()` | Get single cluster details | +| `/:id/` | PUT/PATCH | `update()` | Update cluster | +| `/:id/` | DELETE | `destroy()` | Delete cluster | +| `/bulk_delete/` | POST | `bulk_delete()` | Delete multiple clusters | +| `/bulk_update_status/` | POST | `bulk_update_status()` | Update status for multiple clusters | +| `/auto_generate_ideas/` | POST | `auto_generate_ideas()` | AI idea generation function | + +**ViewSet:** `ClusterViewSet` in `backend/igny8_core/modules/planner/views.py` + +--- + +#### Content Ideas API +**Base URL:** `/v1/planner/content-ideas/` + +| Endpoint | Method | Function | Description | +|----------|--------|----------|-------------| +| `/` | GET | `list()` | List ideas with filters/pagination | +| `/` | POST | `create()` | Create new idea | +| `/:id/` | GET | `retrieve()` | Get single idea details | +| `/:id/` | PUT/PATCH | `update()` | Update idea | +| `/:id/` | DELETE | `destroy()` | Delete idea | +| `/bulk_delete/` | POST | `bulk_delete()` | Delete multiple ideas | +| `/bulk_update_status/` | POST | `bulk_update_status()` | Update status for multiple ideas | +| `/bulk_queue_to_writer/` | POST | `bulk_queue_to_writer()` | Convert ideas to tasks | + +**ViewSet:** `ContentIdeasViewSet` in `backend/igny8_core/modules/planner/views.py` + +--- + +### Writer Module Endpoints + +#### Tasks API +**Base URL:** `/v1/writer/tasks/` + +| Endpoint | Method | Function | Description | +|----------|--------|----------|-------------| +| `/` | GET | `list()` | List tasks with filters/pagination | +| `/` | POST | `create()` | Create new task | +| `/:id/` | GET | `retrieve()` | Get single task details | +| `/:id/` | PUT/PATCH | `update()` | Update task | +| `/:id/` | DELETE | `destroy()` | Delete task | +| `/bulk_delete/` | POST | `bulk_delete()` | Delete multiple tasks | +| `/bulk_update_status/` | POST | `bulk_update_status()` | Update status for multiple tasks | +| `/auto_generate_content/` | POST | `auto_generate_content()` | AI content generation function | + +**ViewSet:** `TasksViewSet` in `backend/igny8_core/modules/writer/views.py` + +--- + +#### Content API +**Base URL:** `/v1/writer/content/` + +| Endpoint | Method | Function | Description | +|----------|--------|----------|-------------| +| `/` | GET | `list()` | List content with filters/pagination | +| `/:id/` | GET | `retrieve()` | Get single content details | +| `/:id/` | PUT/PATCH | `update()` | Update content | +| `/:id/` | DELETE | `destroy()` | Delete content | +| `/generate_image_prompts/` | POST | `generate_image_prompts()` | AI image prompt generation | + +**ViewSet:** `ContentViewSet` in `backend/igny8_core/modules/writer/views.py` + +--- + +#### Images API +**Base URL:** `/v1/writer/images/` + +| Endpoint | Method | Function | Description | +|----------|--------|----------|-------------| +| `/` | GET | `list()` | List images with filters/pagination | +| `/content/` | GET | `by_content()` | List images grouped by content | +| `/settings/` | GET | `get_settings()` | Get image generation settings | +| `/auto_generate/` | POST | `auto_generate_images()` | AI bulk image generation (deprecated) | +| `/generate_images/` | POST | `generate_images()` | Generate images for content | +| `/bulk_update_status/` | POST | `bulk_update_status()` | Update status for content images | + +**ViewSet:** `ImagesViewSet` in `backend/igny8_core/modules/writer/views.py` + +--- + +## Complete Workflow + +### Stage 1: Keyword Research & Clustering + +#### Step 1.1: Attach SeedKeywords to Site +**Frontend:** Keywords Page → "Add Keyword" button +**Function:** `handleCreateSeedKeyword()` +**API Call:** `POST /v1/planner/keywords/` + +**Request Payload:** +```json +{ + "seed_keyword_id": 123, + "site_id": 1, + "sector_id": 2, + "volume_override": null, + "difficulty_override": null, + "cluster_id": null, + "status": "pending" +} +``` + +**Backend Process:** +1. Validate `seed_keyword_id` exists in `seed_keywords` table +2. Validate `site_id` and `sector_id` exist and match +3. Check unique constraint: `seed_keyword + site + sector` +4. Create `Keywords` record with account/site/sector +5. Return created keyword with computed properties + +**Response:** +```json +{ + "success": true, + "data": { + "id": 456, + "seed_keyword_id": 123, + "keyword": "best coffee beans", + "volume": 12000, + "difficulty": 45, + "intent": "commercial", + "cluster_id": null, + "status": "pending", + "created_at": "2025-11-24T10:30:00Z" + }, + "message": "Keyword attached successfully" +} +``` + +--- + +#### Step 1.2: AI Auto-Clustering +**Frontend:** Keywords Page → Select keywords → Bulk Actions → "Auto-Cluster" +**Function:** `handleBulkAction('auto_cluster', ids)` +**API Call:** `POST /v1/planner/keywords/auto_cluster/` + +**Request Payload:** +```json +{ + "ids": [456, 457, 458, 459, 460], + "sector_id": 2 +} +``` + +**Backend Process (`auto_cluster()` view):** +```python +# backend/igny8_core/modules/planner/views.py line 573 +def auto_cluster(self, request): + keyword_ids = request.data.get('ids', []) + sector_id = request.data.get('sector_id') + account = getattr(request, 'account', None) + + # Use ClusteringService + service = ClusteringService() + result = service.cluster_keywords(keyword_ids, account, sector_id) + + if result.get('success'): + if 'task_id' in result: + # Async task queued + return success_response(data={'task_id': result['task_id']}, message='Clustering started') + else: + # Synchronous execution + return success_response(data=result) +``` + +**ClusteringService Logic:** +**Location:** `backend/igny8_core/business/planning/services/clustering_service.py` + +1. **Load Keywords:** + - Fetch `Keywords` by IDs + - Get keyword text, volume, difficulty, intent from seed_keyword + +2. **Check Credits:** + - Calculate credits needed (based on keyword count) + - Deduct credits from account + +3. **AI Clustering:** + - Send keywords to AI engine + - Prompt: "Group these keywords into semantic clusters..." + - AI returns: `{ clusters: [{ name, keywords: [ids] }] }` + +4. **Create/Update Clusters:** + - For each AI cluster: + - Check if cluster with same name exists + - If exists, use existing; else create new `Clusters` record + - Update `Keywords.cluster_id` for all keywords in group + - Calculate `keywords_count`, `volume`, `difficulty` for cluster + +5. **Return Result:** + - Sync: `{ success: true, clusters_created: 3, keywords_updated: 20 }` + - Async: `{ success: true, task_id: 'uuid', message: 'Clustering started' }` + +**Response (Async):** +```json +{ + "success": true, + "data": { + "task_id": "abc123-def456-ghi789" + }, + "message": "Clustering started" +} +``` + +**Frontend Progress Tracking:** +```typescript +// Opens ProgressModal +progressModal.openModal(result.task_id, 'Auto-Clustering Keywords', 'ai-auto-cluster-01'); + +// ProgressModal polls /v1/system/task-progress/:task_id/ every 2 seconds +// Shows: phase, percentage, message, step logs +``` + +**AI Task Progress Phases:** +1. **Initializing** (0-10%) - Validating keywords, loading data +2. **Processing** (10-40%) - Sending to AI engine +3. **AI Analysis** (40-80%) - AI clustering keywords +4. **Saving Results** (80-95%) - Creating clusters, updating keywords +5. **Completed** (100%) - Done + +--- + +### Stage 2: Content Idea Generation + +#### Step 2.1: AI Generate Ideas from Clusters +**Frontend:** Clusters Page → Select clusters → Bulk Actions → "Generate Ideas" +**Function:** `handleBulkAction('auto_generate_ideas', ids)` +**API Call:** `POST /v1/planner/clusters/auto_generate_ideas/` + +**Request Payload:** +```json +{ + "ids": [10, 11, 12] +} +``` + +**Backend Process (`auto_generate_ideas()` view):** +```python +# backend/igny8_core/modules/planner/views.py line 810 +def auto_generate_ideas(self, request): + cluster_ids = request.data.get('ids', []) + account = getattr(request, 'account', None) + + # Use IdeasService + service = IdeasService() + result = service.generate_ideas(cluster_ids, account) + + if result.get('success'): + if 'task_id' in result: + # Async task queued + return success_response(data={'task_id': result['task_id']}, message='Idea generation started') + else: + # Synchronous execution + return success_response(data=result) +``` + +**IdeasService Logic:** +**Location:** `backend/igny8_core/business/planning/services/ideas_service.py` + +1. **Load Clusters:** + - Fetch `Clusters` by IDs with keywords + - Get cluster name, description, keywords, volume + +2. **Check Credits:** + - Calculate credits needed (based on cluster count) + - Deduct credits from account + +3. **AI Idea Generation:** + - For each cluster: + - Send cluster data to AI engine + - Prompt: "Generate 5-10 content ideas for cluster '{name}' with keywords [{keywords}]..." + - AI returns: `[{ title, description, target_keywords, entity_type, cluster_role }]` + +4. **Create Ideas:** + - For each AI idea: + - Create `ContentIdeas` record + - Set `keyword_cluster_id` = cluster ID + - Set `status` = 'new' + - Set `site_entity_type`, `cluster_role` from AI + - Link keywords via `keyword_objects` M2M + +5. **Return Result:** + - Sync: `{ success: true, ideas_created: 25 }` + - Async: `{ success: true, task_id: 'uuid', message: 'Idea generation started' }` + +**Response (Async):** +```json +{ + "success": true, + "data": { + "task_id": "xyz789-uvw456-rst123" + }, + "message": "Idea generation started" +} +``` + +**AI Task Progress Phases:** +1. **Initializing** (0-10%) - Loading clusters, keywords +2. **Analyzing Clusters** (10-30%) - Analyzing keyword patterns +3. **Generating Ideas** (30-90%) - AI creating ideas (per cluster) +4. **Saving Ideas** (90-100%) - Creating ContentIdeas records + +--- + +#### Step 2.2: Queue Ideas to Writer +**Frontend:** Ideas Page → Select ideas (status='new') → Bulk Actions → "Queue to Writer" +**Function:** `handleBulkAction('queue_to_writer', ids)` +**API Call:** `POST /v1/planner/content-ideas/bulk_queue_to_writer/` + +**Request Payload:** +```json +{ + "ids": [50, 51, 52] +} +``` + +**Backend Process (`bulk_queue_to_writer()` view):** +```python +# backend/igny8_core/modules/planner/views.py line 997 +def bulk_queue_to_writer(self, request): + ids = request.data.get('ids', []) + queryset = self.get_queryset() + ideas = queryset.filter(id__in=ids, status='new') # Only 'new' ideas + + created_tasks = [] + for idea in ideas: + task = Tasks.objects.create( + title=idea.idea_title, + description=idea.description or '', + keywords=idea.target_keywords or '', + cluster=idea.keyword_cluster, + idea=idea, + status='queued', + account=idea.account, + site=idea.site, + sector=idea.sector, + entity_type=(idea.site_entity_type or 'post'), + taxonomy=idea.taxonomy, + cluster_role=(idea.cluster_role or 'hub'), + ) + created_tasks.append(task.id) + + # Update idea status to 'scheduled' + idea.status = 'scheduled' + idea.save() + + return success_response(data={'created_count': len(created_tasks)}) +``` + +**Response:** +```json +{ + "success": true, + "data": { + "created_count": 3 + }, + "message": "Queue complete: 3 tasks created from 3 ideas" +} +``` + +--- + +### Stage 3: Content Generation + +#### Step 3.1: AI Generate Content for Task +**Frontend:** Tasks Page → Row Action → "Generate Content" +**Function:** `handleRowAction('generate_content', task)` +**API Call:** `POST /v1/writer/tasks/auto_generate_content/` + +**Request Payload:** +```json +{ + "ids": [100] +} +``` + +**Backend Process (`auto_generate_content()` view):** +```python +# backend/igny8_core/modules/writer/views.py line 150 +def auto_generate_content(self, request): + ids = request.data.get('ids', []) + account = getattr(request, 'account', None) + + # Validate tasks exist + existing_tasks = queryset.filter(id__in=ids, account=account) + + # Use ContentGenerationService + service = ContentGenerationService() + result = service.generate_content(ids, account) + + if result.get('success'): + if 'task_id' in result: + # Async task queued + return success_response(data={'task_id': result['task_id']}, message='Content generation started') + else: + # Synchronous execution + return success_response(data=result, message='Content generated successfully') +``` + +**ContentGenerationService Logic:** +**Location:** `backend/igny8_core/business/content/services/content_generation_service.py` + +1. **Load Tasks:** + - Fetch `Tasks` by IDs with cluster, keywords + - Validate task status (can generate for any status) + +2. **Check Credits:** + - Calculate credits needed (based on word count target) + - Deduct credits from account + +3. **AI Content Generation:** + - For each task: + - Build AI prompt with: + - Title: `task.title` + - Keywords: `task.keywords` or cluster keywords + - Entity type: `task.entity_type` + - Cluster role: `task.cluster_role` + - Word count: estimate from idea or default + - Send to AI engine + - AI returns: `{ html_content, word_count, meta_title, meta_description, primary_keyword, secondary_keywords }` + +4. **Create/Update Content:** + - Check if `task.content_record` exists (OneToOne) + - If exists: update content + - If not: create new `Content` record + - Set fields: + - `html_content` = AI output + - `word_count` = AI calculated + - `meta_title`, `meta_description` = AI SEO + - `primary_keyword`, `secondary_keywords` = AI keywords + - `status` = 'draft' + - `source` = 'igny8' + - `sync_status` = 'native' + - `entity_type` = `task.entity_type` + - `cluster_role` = `task.cluster_role` + - `cluster` = `task.cluster` + - `structure_data` = AI metadata + +5. **Update Task Status:** + - Set `task.status` = 'completed' + +6. **Return Result:** + - Sync: `{ success: true, tasks_updated: 1 }` + - Async: `{ success: true, task_id: 'uuid', message: 'Content generation started' }` + +**Response (Async):** +```json +{ + "success": true, + "data": { + "task_id": "content-abc123-def456" + }, + "message": "Content generation started" +} +``` + +**AI Task Progress Phases:** +1. **Initializing** (0-10%) - Loading tasks, keywords +2. **Research** (10-30%) - Analyzing keywords, cluster +3. **Outlining** (30-50%) - Creating content structure +4. **Writing** (50-90%) - Generating content sections +5. **SEO Optimization** (90-95%) - Adding meta tags, keywords +6. **Saving** (95-100%) - Creating Content record + +--- + +### Stage 4: Image Generation + +#### Step 4.1: Generate Image Prompts (Smart Prompts) +**Frontend:** Content Page → Row Action → "Generate Image Prompts" +**Function:** `handleRowAction('generate_image_prompts', content)` +**API Call:** `POST /v1/writer/content/generate_image_prompts/` + +**Request Payload:** +```json +{ + "ids": [200] +} +``` + +**Backend Process (`generate_image_prompts()` view):** +```python +# backend/igny8_core/modules/writer/views.py line 793 +@action(detail=False, methods=['post'], url_path='generate_image_prompts', url_name='generate_image_prompts') +def generate_image_prompts(self, request): + ids = request.data.get('ids', []) + account = getattr(request, 'account', None) + + # Use ImagePromptService + service = ImagePromptService() + result = service.generate_prompts(ids, account) + + if result.get('success'): + if 'task_id' in result: + return success_response(data={'task_id': result['task_id']}, message='Prompt generation started') + else: + return success_response(data=result) +``` + +**ImagePromptService Logic:** + +1. **Load Content:** + - Fetch `Content` by IDs with `html_content` + - Parse HTML to identify image placement needs + +2. **Analyze Content:** + - Identify sections needing images + - Extract context for each image (surrounding text) + - Determine image type (featured, in-article) + +3. **AI Prompt Generation:** + - For each image placement: + - Send section context to AI + - Prompt: "Create a detailed image prompt for section about '{context}'..." + - AI returns: detailed DALL-E/Stable Diffusion prompt + +4. **Create Image Records:** + - For featured image: create `Images` record with `image_type='featured'` + - For in-article images: create `Images` records with `image_type='in_article'`, `position=N` + - Set `prompt` = AI output + - Set `status` = 'pending' + - Set `provider`, `model` from settings + +5. **Return Result:** + - `{ success: true, prompts_created: 5 }` + +--- + +#### Step 4.2: Generate Images from Prompts +**Frontend:** Images Page → Row Action → "Generate Images" +**Function:** `handleGenerateImages(contentId)` +**Opens:** `ImageQueueModal` + +**Modal Process:** +1. **Fetch Settings:** + - `GET /v1/writer/images/settings/` + - Returns: `{ max_in_article_images, default_provider, default_model }` + +2. **Build Queue:** + - Load content images: `fetchContentImages({ content_id: contentId })` + - Filter images with `status='pending'` and `prompt` + - Order: featured first, then in-article by position + - Limit: max_in_article_images from settings + +3. **Generate Images Sequentially:** + - For each queue item: + - Update UI: status='generating', progress=10% + - Call: `POST /v1/writer/images/generate_images/` + ```json + { + "content_id": 200, + "image_type": "featured", + "prompt": "Professional coffee beans in burlap sack...", + "provider": "dall-e", + "model": "dall-e-3" + } + ``` + - Backend calls image provider API (DALL-E, Stable Diffusion) + - Updates `Images` record: + - `image_url` = provider response + - `status` = 'generated' or 'failed' + - `error_message` = if failed + - Update UI: status='generated', progress=100%, imageUrl=... + +4. **Completion:** + - All images generated + - Update overall status badge + - Reload images list + +**Backend Image Generation:** +```python +# backend/igny8_core/modules/writer/views.py line 634 +@action(detail=False, methods=['post'], url_path='generate_images', url_name='generate_images') +def generate_images(self, request): + content_id = request.data.get('content_id') + image_type = request.data.get('image_type') + prompt = request.data.get('prompt') + provider = request.data.get('provider', 'dall-e') + model = request.data.get('model', 'dall-e-3') + + # Deduct credits + # Call image provider API + # Update Images record with image_url or error + + return success_response(data={'image_url': 'https://...'}) +``` + +--- + +## AI Functions + +### 1. Auto-Cluster Keywords +**Function ID:** `ai-auto-cluster-01` +**API Endpoint:** `POST /v1/planner/keywords/auto_cluster/` +**Service:** `ClusteringService` +**Location:** `backend/igny8_core/business/planning/services/clustering_service.py` + +**Input:** +- `ids`: Keyword IDs (max 20) +- `sector_id`: Sector ID + +**Process:** +1. Load keywords with seed_keyword data +2. Check credits (1 credit per 5 keywords) +3. Send to AI: keyword texts, volumes, difficulties, intents +4. AI groups keywords into semantic clusters +5. Create/update Clusters records +6. Update Keywords.cluster_id + +**Output:** +- Async: `{ task_id }` +- Sync: `{ clusters_created, keywords_updated }` + +**Credits:** 1 credit per 5 keywords (rounded up) + +--- + +### 2. Auto-Generate Ideas +**Function ID:** `ai-generate-ideas-01-desktop` +**API Endpoint:** `POST /v1/planner/clusters/auto_generate_ideas/` +**Service:** `IdeasService` +**Location:** `backend/igny8_core/business/planning/services/ideas_service.py` + +**Input:** +- `ids`: Cluster IDs (max 5) + +**Process:** +1. Load clusters with keywords +2. Check credits (2 credits per cluster) +3. For each cluster: + - Send cluster name, keywords, volumes to AI + - AI generates 5-10 content ideas +4. Create ContentIdeas records + +**Output:** +- Async: `{ task_id }` +- Sync: `{ ideas_created }` + +**Credits:** 2 credits per cluster + +--- + +### 3. Auto-Generate Content +**Function ID:** `ai-generate-content-03` +**API Endpoint:** `POST /v1/writer/tasks/auto_generate_content/` +**Service:** `ContentGenerationService` +**Location:** `backend/igny8_core/business/content/services/content_generation_service.py` + +**Input:** +- `ids`: Task IDs (max 10) + +**Process:** +1. Load tasks with clusters, keywords +2. Check credits (based on word count target) +3. For each task: + - Build AI prompt with title, keywords, entity_type + - AI generates HTML content, meta tags, SEO +4. Create/update Content records +5. Update task status to 'completed' + +**Output:** +- Async: `{ task_id }` +- Sync: `{ tasks_updated }` + +**Credits:** 1 credit per 500 words (estimated) + +--- + +### 4. Generate Image Prompts +**Function ID:** `ai-generate-image-prompts-01-desktop` +**API Endpoint:** `POST /v1/writer/content/generate_image_prompts/` +**Service:** `ImagePromptService` +**Location:** `backend/igny8_core/business/content/services/image_prompt_service.py` + +**Input:** +- `ids`: Content IDs + +**Process:** +1. Load content with HTML +2. Analyze content sections +3. For each image placement: + - Send section context to AI + - AI creates detailed image prompt +4. Create Images records with prompts + +**Output:** +- Async: `{ task_id }` +- Sync: `{ prompts_created }` + +**Credits:** 0.5 credits per prompt + +--- + +### 5. Generate Images +**Function ID:** N/A (synchronous) +**API Endpoint:** `POST /v1/writer/images/generate_images/` +**Service:** Direct image provider call + +**Input:** +- `content_id`: Content ID +- `image_type`: 'featured' or 'in_article' +- `prompt`: Image generation prompt +- `provider`: 'dall-e', 'stable-diffusion', 'midjourney' +- `model`: Provider-specific model + +**Process:** +1. Deduct credits (based on provider/model) +2. Call image provider API (DALL-E, Stable Diffusion) +3. Update Images record with image_url or error + +**Output:** +- `{ image_url: 'https://...' }` + +**Credits:** +- DALL-E 3: 4 credits per image +- DALL-E 2: 2 credits per image +- Stable Diffusion: 1 credit per image + +--- + +## Flowchart Diagrams + +### Complete Workflow Flowchart + +```mermaid +graph TD + Start([User starts workflow]) --> SelectSite[Select Site & Sector] + SelectSite --> AttachKeywords[Attach SeedKeywords
POST /keywords/] + + AttachKeywords --> ManualCluster{Manual or
AI Clustering?} + ManualCluster -->|Manual| EditKeywords[Edit Keywords
Assign to Cluster] + ManualCluster -->|AI| AutoCluster[Auto-Cluster
POST /keywords/auto_cluster/] + + AutoCluster --> ClusteringProgress{Async
Task?} + ClusteringProgress -->|Yes| PollClustering[Poll Progress
GET /task-progress/:id/] + ClusteringProgress -->|No| ClustersCreated[Clusters Created] + PollClustering --> ClustersCreated + EditKeywords --> ClustersCreated + + ClustersCreated --> ManualIdeas{Manual or
AI Ideas?} + ManualIdeas -->|Manual| CreateIdeas[Create Ideas Manually
POST /content-ideas/] + ManualIdeas -->|AI| AutoIdeas[Auto-Generate Ideas
POST /clusters/auto_generate_ideas/] + + AutoIdeas --> IdeasProgress{Async
Task?} + IdeasProgress -->|Yes| PollIdeas[Poll Progress
GET /task-progress/:id/] + IdeasProgress -->|No| IdeasCreated[Ideas Created] + PollIdeas --> IdeasCreated + CreateIdeas --> IdeasCreated + + IdeasCreated --> QueueToWriter[Queue to Writer
POST /content-ideas/bulk_queue_to_writer/] + QueueToWriter --> TasksCreated[Tasks Created
status='queued'] + + TasksCreated --> ManualContent{Manual or
AI Content?} + ManualContent -->|Manual| WriteContent[Write Content Manually] + ManualContent -->|AI| GenerateContent[Generate Content
POST /tasks/auto_generate_content/] + + GenerateContent --> ContentProgress{Async
Task?} + ContentProgress -->|Yes| PollContent[Poll Progress
GET /task-progress/:id/] + ContentProgress -->|No| ContentCreated[Content Created] + PollContent --> ContentCreated + WriteContent --> ContentCreated + + ContentCreated --> ImagePrompts{Generate
Image Prompts?} + ImagePrompts -->|Yes| AIPrompts[Generate Image Prompts
POST /content/generate_image_prompts/] + ImagePrompts -->|No| ManualPrompts[Write Prompts Manually] + + AIPrompts --> PromptsCreated[Image Records Created
status='pending'] + ManualPrompts --> PromptsCreated + + PromptsCreated --> GenerateImages{Generate
Images?} + GenerateImages -->|Yes| ImageQueue[Open ImageQueueModal
Sequential Generation] + GenerateImages -->|No| SkipImages[Skip Images] + + ImageQueue --> ImageLoop[For Each Image:
POST /images/generate_images/] + ImageLoop --> ImageCreated[Image Generated
status='generated'] + ImageCreated --> MoreImages{More
Images?} + MoreImages -->|Yes| ImageLoop + MoreImages -->|No| AllImagesComplete[All Images Complete] + + AllImagesComplete --> ReviewContent[Review Content
status='review'] + SkipImages --> ReviewContent + + ReviewContent --> PublishContent[Publish Content
status='publish'] + PublishContent --> End([Workflow Complete]) + + %% Styling + classDef aiFunction fill:#9f7aea,stroke:#805ad5,stroke-width:2px,color:#fff + classDef userAction fill:#48bb78,stroke:#38a169,stroke-width:2px,color:#fff + classDef decision fill:#ed8936,stroke:#dd6b20,stroke-width:2px,color:#fff + classDef progress fill:#4299e1,stroke:#3182ce,stroke-width:2px,color:#fff + + class AutoCluster,AutoIdeas,GenerateContent,AIPrompts,ImageQueue aiFunction + class AttachKeywords,EditKeywords,CreateIdeas,WriteContent,ManualPrompts,ReviewContent,PublishContent userAction + class ManualCluster,ManualIdeas,ManualContent,ImagePrompts,GenerateImages,ClusteringProgress,IdeasProgress,ContentProgress,MoreImages decision + class PollClustering,PollIdeas,PollContent progress +``` + +--- + +### AI Function Progress Tracking + +```mermaid +sequenceDiagram + participant U as User (Frontend) + participant A as API Endpoint + participant S as Service Layer + participant AI as AI Engine + participant C as Celery Worker + participant DB as Database + + U->>A: POST /keywords/auto_cluster/ {ids: [1,2,3]} + A->>S: ClusteringService.cluster_keywords() + S->>DB: Check credits + DB-->>S: Credits sufficient + S->>C: Queue async task + C-->>S: task_id + S-->>A: {success: true, task_id: 'abc123'} + A-->>U: {task_id: 'abc123', message: 'Clustering started'} + + U->>U: Open ProgressModal + + loop Poll every 2 seconds + U->>A: GET /task-progress/abc123/ + A->>DB: Get task progress + DB-->>A: {phase, percentage, message} + A-->>U: {status, percentage, message, details} + U->>U: Update progress bar + end + + Note over C,AI: Celery worker processes task + C->>AI: Send keywords to AI + AI-->>C: {clusters: [{name, keywords}]} + C->>DB: Create Clusters, update Keywords + DB-->>C: Success + C->>DB: Update task progress: 100%, completed + + U->>A: GET /task-progress/abc123/ + A->>DB: Get task progress + DB-->>A: {status: 'completed', percentage: 100} + A-->>U: {status: 'completed', percentage: 100} + U->>U: Close modal, show success toast + U->>A: GET /keywords/ (reload data) + A->>DB: Fetch updated keywords + DB-->>A: Keywords with cluster_id set + A-->>U: Updated keywords list +``` + +--- + +### Image Generation Queue Flow + +```mermaid +sequenceDiagram + participant U as User (Frontend) + participant M as ImageQueueModal + participant A as API Endpoint + participant P as Image Provider + participant DB as Database + + U->>A: GET /images/content/?content_id=200 + A->>DB: Fetch content images + DB-->>A: {featured_image, in_article_images[]} + A-->>U: Content images data + + U->>U: Click "Generate Images" + U->>A: GET /images/settings/ + A-->>U: {max_in_article_images: 3, default_provider: 'dall-e'} + + U->>M: Open ImageQueueModal + M->>M: Build queue: [featured, in-article-1, in-article-2, in-article-3] + M->>U: Display queue items (status: pending) + + loop For each queue item + M->>M: Update UI: status='generating', progress=10% + M->>A: POST /images/generate_images/ {content_id, image_type, prompt, provider, model} + A->>DB: Deduct credits + A->>P: Generate image (DALL-E API) + P-->>A: {image_url: 'https://...'} + A->>DB: Update Images record (image_url, status='generated') + DB-->>A: Success + A-->>M: {success: true, image_url: 'https://...'} + M->>M: Update UI: status='generated', progress=100%, show image + end + + M->>M: All images complete + M->>U: Show success message, close modal option + U->>U: Close modal + U->>A: GET /images/content/ (reload) + A->>DB: Fetch updated images + DB-->>A: All images with status='generated' + A-->>U: Updated images list +``` + +--- + +## Summary + +This workflow documentation covers: + +✅ **Database Tables:** All 6 core models with fields, types, indexes, and relationships +✅ **Frontend Pages:** 6 pages with all functions, API calls, and state management +✅ **Backend APIs:** All endpoints with methods, functions, and ViewSets +✅ **Complete Workflow:** Step-by-step from keywords to published content +✅ **AI Functions:** 5 AI functions with inputs, processes, outputs, and credits +✅ **Flowcharts:** 3 detailed diagrams (complete workflow, progress tracking, image generation) + +**Key Workflow Paths:** + +1. **Manual Path:** User creates everything manually (keywords → clusters → ideas → tasks → content) +2. **AI-Assisted Path:** User uses AI functions at each stage (auto-cluster, auto-generate ideas, auto-generate content) +3. **Hybrid Path:** Mix of manual and AI (manual keywords, AI clustering, manual ideas, AI content) + +**Credits System:** +- Auto-Cluster: 1 credit per 5 keywords +- Generate Ideas: 2 credits per cluster +- Generate Content: 1 credit per 500 words +- Image Prompts: 0.5 credits per prompt +- Generate Images: 1-4 credits per image (provider-dependent) + +**Progress Tracking:** +All async AI functions return `task_id`, which is polled via `/task-progress/:id/` to show real-time progress with phases, percentages, and step logs. + +--- + +**End of Documentation** diff --git a/old-docs/igny8-app/05-WRITER-IMAGES-PAGE-SYSTEM-DESIGN.md b/old-docs/igny8-app/05-WRITER-IMAGES-PAGE-SYSTEM-DESIGN.md new file mode 100644 index 00000000..d5e3e90c --- /dev/null +++ b/old-docs/igny8-app/05-WRITER-IMAGES-PAGE-SYSTEM-DESIGN.md @@ -0,0 +1,1228 @@ +# 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([]); +const [loading, setLoading] = useState(true); + +// Filter State +const [searchTerm, setSearchTerm] = useState(''); +const [statusFilter, setStatusFilter] = useState(''); +const [selectedIds, setSelectedIds] = useState([]); + +// 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('content_title'); +const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); + +// Image Generation Modal +const [isQueueModalOpen, setIsQueueModalOpen] = useState(false); +const [imageQueue, setImageQueue] = useState([]); +const [currentContentId, setCurrentContentId] = useState(null); +const [taskId, setTaskId] = useState(null); +const [imageModel, setImageModel] = useState(null); +const [imageProvider, setImageProvider] = useState(null); + +// Status Update Modal +const [isStatusModalOpen, setIsStatusModalOpen] = useState(false); +const [statusUpdateContentId, setStatusUpdateContentId] = useState(null); +const [statusUpdateRecordName, setStatusUpdateRecordName] = useState(''); +const [isUpdatingStatus, setIsUpdatingStatus] = useState(false); + +// Image Preview Modal +const [isImageModalOpen, setIsImageModalOpen] = useState(false); +const [modalImageUrl, setModalImageUrl] = useState(null); + +// Debug State +const [aiLogs, setAiLogs] = useState>([]); // 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 { + 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 { + 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 { + // 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: , + 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: , + variant: 'primary', + }, + ], + bulkActions: [ + { + key: 'bulk_publish_wordpress', + label: 'Publish Ready to WordPress', + icon: , + 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** diff --git a/old-docs/igny8-app/06-FEATURE-MODIFICATION-DEVELOPER-GUIDE.md b/old-docs/igny8-app/06-FEATURE-MODIFICATION-DEVELOPER-GUIDE.md new file mode 100644 index 00000000..a36508a9 --- /dev/null +++ b/old-docs/igny8-app/06-FEATURE-MODIFICATION-DEVELOPER-GUIDE.md @@ -0,0 +1,1121 @@ +# Feature Modification & Development Guide +## Complete File Reference for Adding, Modifying, and Removing Features + +**Version:** 1.0.0 +**Last Updated:** November 29, 2025 +**Purpose:** Quick reference for developers to locate exactly which files to modify for specific feature types, across both frontend and backend. + +--- + +## Table of Contents + +1. [Quick Navigation by Feature Type](#quick-navigation-by-feature-type) +2. [Feature Type Reference Matrix](#feature-type-reference-matrix) +3. [Frontend Feature Modifications](#frontend-feature-modifications) +4. [Backend Feature Modifications](#backend-feature-modifications) +5. [Cross-Module Modifications](#cross-module-modifications) +6. [Database Schema Changes](#database-schema-changes) +7. [Common Workflow Patterns](#common-workflow-patterns) +8. [File Organization Index](#file-organization-index) + +--- + +## Quick Navigation by Feature Type + +### 🎯 Find Your Feature Type + +- **Adding/Removing/Modifying a Database Column** → [Database Changes](#database-changes) +- **Adding a New Page/List View** → [New Page Feature](#new-page-feature) +- **Adding a Column to a Table/List** → [Table Column Addition](#table-column-addition) +- **Adding a Form Field** → [Form Field Addition](#form-field-addition) +- **Adding an API Endpoint** → [API Endpoint Addition](#api-endpoint-addition) +- **Adding a Filter/Search** → [Filter/Search Addition](#filtersearch-addition) +- **Adding a Status/Choice Field** → [Status Field Addition](#status-field-addition) +- **Modifying API Response** → [API Response Modification](#api-response-modification) +- **Adding Validation** → [Validation Addition](#validation-addition) +- **Adding Background Task** → [Background Task Addition](#background-task-addition) +- **Adding Webhook** → [Webhook Addition](#webhook-addition) +- **Adding Integration** → [Integration Addition](#integration-addition) + +--- + +## Feature Type Reference Matrix + +| Feature Type | Frontend Files | Backend Files | Database | Global Config | +|--------------|---|---|---|---| +| **Database Column** | - | Model + Serializer | Migration | - | +| **Table Column** | List Component | ViewSet Filter | - | - | +| **Form Field** | Form Component | Serializer | Model Migration | - | +| **Page/View** | Page Component | ViewSet | - | Routes | +| **API Endpoint** | API Hook | ViewSet + Router | - | urls.py | +| **Status Field** | Select/Enum | Model Choice | Migration | - | +| **Filter/Search** | Filter UI | filterset_fields | Index | - | +| **Validation** | Form Rules | Validator Service | - | - | +| **Background Task** | UI Trigger | Celery Task | - | settings.py | +| **Webhook** | - | Handler Function | WebhookLog Model | urls.py | +| **Integration** | - | Adapter Class | Integration Model | - | + +--- + +## Frontend Feature Modifications + +### Directory Structure + +``` +frontend/src/ +├── pages/ +│ ├── Planner/ +│ │ ├── Dashboard.tsx +│ │ ├── Keywords.tsx +│ │ ├── Clusters.tsx +│ │ ├── ClusterDetail.tsx +│ │ ├── Ideas.tsx +│ │ └── KeywordOpportunities.tsx +│ ├── Writer/ +│ │ ├── Dashboard.tsx +│ │ ├── Tasks.tsx +│ │ ├── Content.tsx +│ │ ├── ContentView.tsx +│ │ ├── Drafts.tsx +│ │ ├── Images.tsx +│ │ ├── Published.tsx +│ │ └── Review.tsx +│ ├── Linker/ +│ │ └── [Linker Pages] +│ ├── Optimizer/ +│ │ └── [Optimizer Pages] +│ ├── Settings/ +│ │ └── [Settings Pages] +│ └── [Other Modules] +├── components/ +│ ├── common/ +│ │ ├── Table.tsx +│ │ ├── Modal.tsx +│ │ ├── Form.tsx +│ │ ├── Button.tsx +│ │ └── [Shared UI Components] +│ ├── [Module-Specific Components] +│ └── [Feature Components] +├── api/ +│ ├── client.ts # Axios setup +│ ├── auth.ts # Auth API calls +│ ├── planner.ts # Planner endpoints +│ ├── writer.ts # Writer endpoints +│ ├── [module].ts # Module-specific APIs +│ └── hooks/ +│ └── useApi.ts # API hook +├── store/ +│ ├── authStore.ts # Auth state +│ ├── siteStore.ts # Site/Sector state +│ ├── [module]Store.ts # Module state +│ └── globalStore.ts # Global state +├── hooks/ +│ ├── useAuth.ts +│ ├── useApi.ts +│ ├── useSite.ts +│ └── [Custom Hooks] +├── types/ +│ ├── api.ts # API type definitions +│ ├── models.ts # Data model types +│ └── [Module Types] +└── utils/ + ├── formatters.ts + ├── validators.ts + └── [Utility Functions] +``` + +### Feature: Adding a Column to a List/Table + +**Module Example: Writer/Tasks List** + +#### Files to Modify: + +| Layer | File | Change Type | Action | +|-------|------|-------------|--------| +| **Data Type** | `frontend/src/types/models.ts` | Modify | Add field to Task interface | +| **API Response** | `frontend/src/api/writer.ts` | Auto | API will return new field from backend | +| **Page Component** | `frontend/src/pages/Writer/Tasks.tsx` | Modify | Add column to table columns config | +| **Table Component** | `frontend/src/components/Writer/TasksTable.tsx` | Modify | Render new column in JSX | +| **Backend ViewSet** | `backend/igny8_core/modules/writer/views.py` | Modify | Add to `filterset_fields` if filterable, `ordering_fields` if sortable | +| **Backend Serializer** | `backend/igny8_core/modules/writer/serializers.py` | Modify | Add field to serializer `fields` list | +| **Backend Model** | `backend/igny8_core/business/content/models.py` | Modify | Add field to Tasks model class | +| **Backend Migration** | `backend/igny8_core/migrations/` | Create | `python manage.py makemigrations` | + +#### Step-by-Step for "Adding 'priority' column to Tasks list": + +```python +# Step 1: Backend Model (models.py) +class Tasks(SiteSectorBaseModel): + # ... existing fields ... + priority = models.IntegerField( + choices=[(1, 'Low'), (2, 'Medium'), (3, 'High')], + default=2 + ) + +# Step 2: Backend Serializer (serializers.py) +class TasksSerializer(serializers.ModelSerializer): + class Meta: + model = Tasks + fields = ['id', 'title', 'status', 'priority', ...] # Add 'priority' + +# Step 3: Backend ViewSet (views.py) - if filterable/sortable +class TasksViewSet(SiteSectorModelViewSet): + filterset_fields = ['status', 'cluster_id', 'priority'] # Add 'priority' + ordering_fields = ['title', 'created_at', 'priority'] # Add 'priority' + +# Step 4: Frontend Type (types/models.ts) +interface Task { + id: number; + title: string; + status: string; + priority: number; // Add this + // ... other fields +} + +# Step 5: Frontend API (api/writer.ts) - No changes needed if using generic fetch + +# Step 6: Frontend Component (pages/Writer/Tasks.tsx) +const columns = [ + { key: 'id', label: 'ID', width: '10%' }, + { key: 'title', label: 'Title', width: '40%' }, + { key: 'status', label: 'Status', width: '20%' }, + { key: 'priority', label: 'Priority', width: '15%' }, // Add this +]; + +# Step 7: Database Migration +# bash: python manage.py makemigrations +# bash: python manage.py migrate +``` + +### Feature: Adding a Form Field + +**Module Example: Writer/Tasks Create Form** + +#### Files to Modify: + +| Layer | File | Change Type | Description | +|-------|------|-------------|-------------| +| **Backend Model** | `backend/.../models.py` | Add | New model field with validators | +| **Backend Serializer** | `backend/.../serializers.py` | Add | Serializer field with validation | +| **Backend ViewSet** | `backend/.../views.py` | Modify | Add to `filterset_fields` if needed | +| **Frontend Type** | `frontend/src/types/models.ts` | Add | Add to interface | +| **Frontend Form Component** | `frontend/src/pages/Writer/[FormPage].tsx` | Add | Add form input JSX | +| **Frontend Validation** | `frontend/src/utils/validators.ts` | Add | Client-side validation rule | +| **Frontend State** | `frontend/src/store/[module]Store.ts` | Add | Add to form state if needed | +| **Database Migration** | `backend/migrations/` | Create | If model field is added | + +#### Example: "Adding 'estimatedWordCount' field to Tasks form": + +```python +# Step 1: Backend Model +class Tasks(SiteSectorBaseModel): + # ... existing fields ... + word_count = models.IntegerField( + default=1000, + validators=[MinValueValidator(100), MaxValueValidator(10000)], + help_text="Target word count (100-10000)" + ) + +# Step 2: Backend Serializer +class TasksSerializer(serializers.ModelSerializer): + word_count = serializers.IntegerField( + required=False, + min_value=100, + max_value=10000 + ) + + class Meta: + model = Tasks + fields = [..., 'word_count'] + +# Step 3: Frontend Type +interface Task { + id: number; + title: string; + wordCount: number; // Add this (camelCase) +} + +# Step 4: Frontend Form Component +function TaskForm() { + const [formData, setFormData] = useState({ + title: '', + wordCount: 1000, // Add this + }); + + return ( +
+ {/* ... other fields ... */} + setFormData({...formData, wordCount: e.target.value})} + /> +
+ ); +} + +# Step 5: Frontend Validation +export const validateTaskForm = (data) => { + if (data.wordCount < 100 || data.wordCount > 10000) { + return 'Word count must be between 100 and 10000'; + } + return null; +}; +``` + +### Feature: Adding a New Page/View + +**Module Example: Adding "Writer/Scheduled" Page** + +#### Files to Create/Modify: + +| Layer | File | Change Type | Action | +|-------|------|-------------|--------| +| **New Page Component** | `frontend/src/pages/Writer/Scheduled.tsx` | Create | New React component | +| **Page Router** | `frontend/src/App.tsx` | Modify | Add route entry | +| **Navigation** | `frontend/src/layout/Sidebar.tsx` | Modify | Add menu item | +| **API Hooks** | `frontend/src/api/writer.ts` | Modify | Add API call if needed | +| **Type Definitions** | `frontend/src/types/models.ts` | Modify | Add types if needed | + +#### Step-by-Step for "Adding Writer/Scheduled Page": + +```typescript +// Step 1: Create new page component +// File: frontend/src/pages/Writer/Scheduled.tsx + +import React, { useEffect, useState } from 'react'; +import { getScheduledContent } from '../../api/writer'; +import ContentTable from '../../components/Writer/ContentTable'; + +export default function ScheduledPage() { + const [content, setContent] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const loadContent = async () => { + try { + const response = await getScheduledContent({ + siteId: currentSite.id, + sectorId: currentSector.id, + status: 'scheduled' + }); + setContent(response.data); + } catch (error) { + console.error('Failed to load scheduled content:', error); + } finally { + setLoading(false); + } + }; + loadContent(); + }, []); + + return ( +
+

Scheduled Content

+ +
+ ); +} + +// Step 2: Add route in App.tsx +import ScheduledPage from './pages/Writer/Scheduled'; + +function App() { + return ( + + {/* ... other routes ... */} + } /> + + ); +} + +// Step 3: Add to Sidebar navigation +// File: frontend/src/layout/Sidebar.tsx +} +/> + +// Step 4: Add API function if needed +// File: frontend/src/api/writer.ts +export const getScheduledContent = async (params) => { + return fetchAPI('/api/v1/writer/content/', { + query: { ...params, status: 'scheduled' } + }); +}; +``` + +### Feature: Adding a Filter/Search + +**Module Example: Adding "Priority Filter" to Tasks** + +#### Files to Modify: + +| Layer | File | Change Type | Description | +|-------|------|-------------|-------------| +| **Backend ViewSet** | `backend/.../views.py` | Modify | Add to `filterset_fields` | +| **Backend Model** | `backend/.../models.py` | Check | Field must have choices | +| **Frontend Filter UI** | `frontend/src/components/Writer/TasksFilter.tsx` | Add | Add filter control | +| **Frontend State** | `frontend/src/store/writerStore.ts` | Modify | Add filter state | + +#### Example: + +```python +# Backend (views.py) +class TasksViewSet(SiteSectorModelViewSet): + filterset_fields = ['status', 'cluster_id', 'priority'] # Add 'priority' + +# Frontend - Add Filter Component +interface TaskFilter { + priority?: number; + status?: string; +} + +function TasksFilter() { + const [filters, setFilters] = useState({}); + + return ( +
+ +
+ ); +} +``` + +--- + +## Backend Feature Modifications + +### Directory Structure + +``` +backend/igny8_core/ +├── modules/ # API ViewSets (REST endpoints) +│ ├── planner/ +│ │ ├── views.py # ViewSets +│ │ ├── serializers.py # Serializers +│ │ ├── urls.py # Route definitions +│ │ └── apps.py +│ ├── writer/ +│ │ ├── views.py +│ │ ├── serializers.py +│ │ ├── urls.py +│ │ └── apps.py +│ ├── [other modules] +│ └── urls.py # Root routing +├── business/ # Business logic & models +│ ├── planning/ +│ │ ├── models.py # Database models +│ │ └── services/ # Business logic +│ ├── content/ +│ │ ├── models.py +│ │ └── services/ +│ ├── [other domains] +│ └── models.py +├── auth/ +│ ├── models.py # User, Account, Site, Sector +│ ├── views.py +│ ├── serializers.py +│ └── urls.py +├── api/ +│ ├── base.py # Base ViewSet classes +│ ├── response.py # Response utilities +│ ├── pagination.py +│ ├── permissions.py +│ ├── throttles.py +│ └── exception_handlers.py +├── ai/ # AI Engine +│ ├── engine.py +│ ├── ai_core.py +│ ├── registry.py +│ └── functions/ +├── middleware/ +├── utils/ +├── tasks/ # Celery tasks +├── migrations/ # Database migrations +├── settings.py # Django settings +├── urls.py # Root URL config +└── wsgi.py +``` + +### Feature: Adding an API Endpoint + +**Module Example: Adding "GET /writer/tasks/{id}/brief/" endpoint** + +#### Files to Create/Modify: + +| File | Change Type | Description | +|------|-------------|-------------| +| `backend/igny8_core/modules/writer/views.py` | Add | Add `@action` method to ViewSet | +| `backend/igny8_core/modules/writer/serializers.py` | Check | Serializer for response | +| `backend/igny8_core/modules/writer/urls.py` | Auto | Routes auto-generated by DRF `DefaultRouter` | + +#### Step-by-Step Example: + +```python +# Step 1: Add action to ViewSet (views.py) +class TasksViewSet(SiteSectorModelViewSet): + # ... existing code ... + + @action(detail=True, methods=['get']) + def brief(self, request, pk=None): + """ + Get task brief + GET /api/v1/writer/tasks/{id}/brief/ + """ + task = self.get_object() + + # Business logic to generate brief + brief_data = { + 'task_id': task.id, + 'title': task.title, + 'keywords': task.keywords, + 'cluster': task.cluster.name if task.cluster else None, + 'description': task.description + } + + return success_response( + success=True, + data=brief_data, + message='Task brief retrieved' + ) + +# Step 2: That's it! DRF auto-generates the route +# URL will be: /api/v1/writer/tasks/123/brief/ +# Method: GET +# Response format: { success: true, data: {...}, message: '...' } + +# Step 3: Frontend API hook +// frontend/src/api/writer.ts +export const getTaskBrief = async (taskId: number) => { + return fetchAPI(`/api/v1/writer/tasks/${taskId}/brief/`); +}; +``` + +### Feature: Adding a Status/Choice Field + +**Module Example: Adding "priority" choice field to Tasks** + +#### Files to Modify: + +| Layer | File | Change Type | Action | +|-------|------|-------------|--------| +| **Backend Model** | `backend/.../models.py` | Add | Add field with CHOICES | +| **Database Migration** | `backend/migrations/` | Create | Auto from model | +| **Backend Serializer** | `backend/.../serializers.py` | Auto | Choices auto-included | +| **Backend ViewSet** | `backend/.../views.py` | Modify | Add to filters if needed | +| **Frontend Types** | `frontend/src/types/models.ts` | Add | Add type definition | +| **Frontend Enum/Constant** | `frontend/src/config/constants.ts` | Add | Add choice options | +| **Frontend UI** | Components using field | Use | Render as select/dropdown | + +#### Example: + +```python +# Step 1: Backend Model (models.py) +class Tasks(SiteSectorBaseModel): + PRIORITY_CHOICES = [ + (1, 'Low'), + (2, 'Medium'), + (3, 'High'), + (4, 'Urgent'), + ] + + priority = models.IntegerField( + choices=PRIORITY_CHOICES, + default=2, + help_text="Task priority level" + ) + +# Step 2: Backend Serializer (serializers.py) - No changes needed! +# DRF automatically includes choices in API response as: +# { +# "priority": 2, +# "priority_display": "Medium", +# "priority_choices": [[1, "Low"], [2, "Medium"], ...] +# } + +# Step 3: Frontend Constant (config/constants.ts) +export const TASK_PRIORITIES = { + LOW: 1, + MEDIUM: 2, + HIGH: 3, + URGENT: 4, +}; + +export const TASK_PRIORITY_LABELS = { + 1: 'Low', + 2: 'Medium', + 3: 'High', + 4: 'Urgent', +}; + +# Step 4: Frontend Type (types/models.ts) +interface Task { + id: number; + priority: 1 | 2 | 3 | 4; // Use enum values +} + +# Step 5: Frontend Component +function TaskSelect() { + return ( + + ); +} +``` + +### Feature: Adding Validation + +**Module Example: Validating task word count (100-10000)** + +#### Files to Create/Modify: + +| File | Change Type | Description | +|------|-------------|-------------| +| `backend/igny8_core/business/content/services/validation_service.py` | Create | Validation logic | +| `backend/igny8_core/modules/writer/serializers.py` | Modify | Add field validation | +| `frontend/src/utils/validators.ts` | Add | Client-side validation | + +#### Example: + +```python +# Step 1: Backend Model Field Validators (models.py) +from django.core.validators import MinValueValidator, MaxValueValidator + +class Tasks(SiteSectorBaseModel): + word_count = models.IntegerField( + default=1000, + validators=[ + MinValueValidator(100, message="Minimum word count is 100"), + MaxValueValidator(10000, message="Maximum word count is 10000") + ] + ) + +# Step 2: Backend Serializer Validation (serializers.py) +class TasksSerializer(serializers.ModelSerializer): + word_count = serializers.IntegerField( + min_value=100, + max_value=10000, + required=False + ) + + def validate_word_count(self, value): + if value and (value < 100 or value > 10000): + raise serializers.ValidationError( + "Word count must be between 100 and 10000" + ) + return value + + class Meta: + model = Tasks + fields = ['id', 'title', 'word_count', ...] + +# Step 3: Frontend Validation (utils/validators.ts) +export const validateTaskWordCount = (wordCount: number): string | null => { + if (wordCount < 100) { + return 'Word count must be at least 100'; + } + if (wordCount > 10000) { + return 'Word count cannot exceed 10000'; + } + return null; +}; + +# Step 4: Frontend Component +function TaskForm() { + const [wordCount, setWordCount] = useState(1000); + const [error, setError] = useState(''); + + const handleChange = (value) => { + setWordCount(value); + const err = validateTaskWordCount(value); + setError(err || ''); + }; + + return ( + <> + handleChange(Number(e.target.value))} + /> + {error && {error}} + + ); +} +``` + +### Feature: Adding a Background Task (Celery) + +**Module Example: Adding "Async content generation" task** + +#### Files to Create/Modify: + +| File | Change Type | Description | +|------|-------------|-------------| +| `backend/igny8_core/tasks/[domain]_tasks.py` | Create | Celery task function | +| `backend/igny8_core/modules/[module]/views.py` | Modify | Queue task from ViewSet | +| `backend/igny8_core/settings.py` | Check | Celery config (usually pre-configured) | + +#### Example: Adding "generate_content_images" async task + +```python +# Step 1: Create Celery Task (tasks/writer_tasks.py) +from celery import shared_task +import logging + +logger = logging.getLogger(__name__) + +@shared_task(bind=True, max_retries=3) +def generate_content_images(self, content_id): + """ + Generate AI images for content + """ + from igny8_core.business.content.models import Content + from igny8_core.ai.engine import AIEngine + + logger.info(f"[generate_content_images] 🎬 Starting image generation for content {content_id}") + + try: + content = Content.objects.get(id=content_id) + + # Generate images + engine = AIEngine() + images = engine.generate_images( + content_id=content.id, + title=content.title, + keywords=content.secondary_keywords + ) + + # Save images + for image_data in images: + Image.objects.create( + content=content, + url=image_data['url'], + prompt=image_data['prompt'] + ) + + logger.info(f"[generate_content_images] ✅ Successfully generated {len(images)} images") + return {'success': True, 'image_count': len(images)} + + except Content.DoesNotExist: + logger.error(f"[generate_content_images] ❌ Content {content_id} not found") + return {'success': False, 'error': 'Content not found'} + except Exception as exc: + logger.error(f"[generate_content_images] ❌ Task failed: {str(exc)}") + # Retry with exponential backoff + raise self.retry(exc=exc, countdown=2 ** self.request.retries) + +# Step 2: Queue task from ViewSet (views.py) +class ContentViewSet(SiteSectorModelViewSet): + @action(detail=True, methods=['post']) + def generate_images(self, request, pk=None): + """ + Trigger async image generation + POST /api/v1/writer/content/{id}/generate_images/ + """ + content = self.get_object() + + # Queue background task + from igny8_core.tasks.writer_tasks import generate_content_images + + task = generate_content_images.delay(content.id) + + return success_response( + success=True, + data={'task_id': task.id}, + message='Image generation started' + ) + +# Step 3: Frontend to trigger task +// frontend/src/api/writer.ts +export const generateContentImages = async (contentId: number) => { + return fetchAPI(`/api/v1/writer/content/${contentId}/generate_images/`, { + method: 'POST' + }); +}; + +// frontend/src/pages/Writer/Content.tsx +async function handleGenerateImages(contentId) { + try { + const response = await generateContentImages(contentId); + toast.success('Image generation started'); + } catch (error) { + toast.error('Failed to start image generation'); + } +} +``` + +--- + +## Database Schema Changes + +### Feature: Adding/Modifying a Database Column + +#### Complete Workflow: + +```bash +# Step 1: Add field to model +# File: backend/igny8_core/business/content/models.py +class Content(SiteSectorBaseModel): + # ... existing fields ... + seo_score = models.IntegerField( + default=0, + validators=[MinValueValidator(0), MaxValueValidator(100)], + help_text="SEO optimization score (0-100)" + ) + +# Step 2: Create migration +$ python manage.py makemigrations writer + +# Output: Migrations for 'writer': +# migrations/0015_content_seo_score.py + +# Step 3: Review migration (optional) +$ cat migrations/0015_content_seo_score.py + +# Step 4: Apply migration +$ python manage.py migrate + +# Step 5: Update serializer to include new field +# File: backend/igny8_core/modules/writer/serializers.py +class ContentSerializer(serializers.ModelSerializer): + class Meta: + model = Content + fields = [ + 'id', 'title', 'content_html', 'status', + 'seo_score', # Add this + # ... other fields ... + ] + +# Step 6: Update frontend types +// frontend/src/types/models.ts +interface Content { + id: number; + title: string; + contentHtml: string; + seoScore: number; // Add this +} + +# Step 7: Update components to use new field +``` + +### Feature: Removing a Database Column + +```python +# Step 1: Create migration to remove field +$ python manage.py makemigrations writer --remove-field + +# Step 2: Or manually edit model - comment out field +class Content(SiteSectorBaseModel): + # ... existing fields ... + # seo_score = models.IntegerField(...) # REMOVED + +# Step 3: Auto-create migration +$ python manage.py makemigrations writer + +# Step 4: Apply migration +$ python manage.py migrate + +# Step 5: Remove from serializer +# backend/igny8_core/modules/writer/serializers.py +# Remove 'seo_score' from fields list + +# Step 6: Remove from frontend types and components +``` + +### Feature: Modifying a Database Column + +```python +# Example: Change word_count max value from 10000 to 20000 + +# Step 1: Update model +class Tasks(SiteSectorBaseModel): + word_count = models.IntegerField( + default=1000, + validators=[MinValueValidator(100), MaxValueValidator(20000)] # Changed + ) + +# Step 2: Create migration +$ python manage.py makemigrations writer + +# Step 3: Apply migration +$ python manage.py migrate + +# Step 4: Update serializer if validation changed +class TasksSerializer(serializers.ModelSerializer): + word_count = serializers.IntegerField( + min_value=100, + max_value=20000 # Update + ) + +# Step 5: Update frontend validation constants +export const MAX_WORD_COUNT = 20000; // Update +``` + +--- + +## Cross-Module Modifications + +### Feature: Adding a Relationship Between Modules + +**Example: Link Content to Ideas (Writer → Planner)** + +#### Files to Modify: + +```python +# Step 1: Backend Model - Add ForeignKey +# backend/igny8_core/business/content/models.py +class Content(SiteSectorBaseModel): + # ... existing fields ... + idea = models.ForeignKey( + 'planner.ContentIdeas', + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='contents', + help_text="Source content idea" + ) + +# Step 2: Create migration +$ python manage.py makemigrations writer + +# Step 3: Apply migration +$ python manage.py migrate + +# Step 4: Update serializer to include nested relationship +# backend/igny8_core/modules/writer/serializers.py +from igny8_core.modules.planner.serializers import ContentIdeasSerializer + +class ContentSerializer(serializers.ModelSerializer): + idea = ContentIdeasSerializer(read_only=True) + idea_id = serializers.PrimaryKeyRelatedField( + queryset=ContentIdeas.objects.all(), + source='idea', + write_only=True + ) + + class Meta: + model = Content + fields = ['id', 'title', 'idea', 'idea_id', ...] + +# Step 5: Update ViewSet to support filtering by idea +# backend/igny8_core/modules/writer/views.py +class ContentViewSet(SiteSectorModelViewSet): + filterset_fields = ['status', 'cluster_id', 'idea_id'] # Add idea_id + ordering_fields = ['title', 'created_at', 'idea_id'] # Add idea_id + +# Step 6: Frontend - Update type +// frontend/src/types/models.ts +interface Content { + id: number; + title: string; + idea?: Idea; // Add nested object + ideaId?: number; // Add ID for create/update +} + +# Step 7: Frontend - Display related idea +// frontend/src/pages/Writer/Content.tsx +{content.idea && ( +
Idea: {content.idea.ideaTitle}
+)} +``` + +--- + +## Common Workflow Patterns + +### Pattern 1: Adding a Simple Field to List Page + +**Timeline: ~15 minutes** + +``` +1. Backend Model → Add field with validators +2. DB Migration → makemigrations + migrate +3. Backend Serializer → Add to fields list +4. Backend ViewSet → Add to filters/ordering if needed +5. Frontend Type → Add to interface +6. Frontend Component → Add to table columns +7. Test → Verify in both UI and API +``` + +**Files Modified:** +- `backend/igny8_core/business/*/models.py` +- `backend/igny8_core/migrations/XXXX_*.py` +- `backend/igny8_core/modules/*/serializers.py` +- `backend/igny8_core/modules/*/views.py` +- `frontend/src/types/models.ts` +- `frontend/src/pages/*/[Page].tsx` + +### Pattern 2: Adding a New Feature Page + +**Timeline: ~1 hour** + +``` +1. Backend ViewSet/Action → Create endpoint +2. Backend Serializer → Define response format +3. Frontend API Hook → Create fetch function +4. Frontend Type → Define data interface +5. Frontend Page Component → Create page +6. Frontend Router → Add route +7. Frontend Navigation → Add menu item +8. Test → End-to-end flow +``` + +**Files Modified:** +- `backend/igny8_core/modules/*/views.py` +- `backend/igny8_core/modules/*/serializers.py` +- `frontend/src/api/*.ts` +- `frontend/src/types/models.ts` +- `frontend/src/pages/*/*.tsx` +- `frontend/src/App.tsx` +- `frontend/src/layout/Sidebar.tsx` + +### Pattern 3: Adding Async Processing + +**Timeline: ~30 minutes** + +``` +1. Backend Celery Task → Create task function +2. Backend ViewSet Action → Queue task from endpoint +3. Frontend API Hook → Create trigger function +4. Frontend Component → Add button to trigger +5. Frontend Polling/Polling → Show task status +6. Test → Verify task execution and completion +``` + +**Files Modified:** +- `backend/igny8_core/tasks/*_tasks.py` +- `backend/igny8_core/modules/*/views.py` +- `frontend/src/api/*.ts` +- `frontend/src/pages/*/*.tsx` + +--- + +## File Organization Index + +### Module File Structure Template + +For each module (Planner, Writer, Linker, etc.): + +``` +backend/igny8_core/ +├── modules/[module]/ +│ ├── views.py # ViewSets, @actions +│ ├── serializers.py # Serializers +│ ├── urls.py # Route registration +│ └── apps.py +└── business/[domain]/ + ├── models.py # Data models + ├── services/ + │ ├── service1.py # Business logic + │ └── service2.py + └── migrations/ + └── XXXX_*.py # DB migrations + +frontend/src/ +├── pages/[Module]/ +│ ├── Dashboard.tsx # Overview page +│ ├── [Feature].tsx # Feature pages +│ └── [Detail].tsx # Detail views +├── components/[Module]/ +│ ├── [Feature]Table.tsx # Table components +│ ├── [Feature]Form.tsx # Form components +│ └── [Feature]Filter.tsx # Filter components +├── api/ +│ └── [module].ts # API calls +├── store/ +│ └── [module]Store.ts # State management +└── types/ + └── models.ts # Type definitions +``` + +### Global Files (All Modules) + +| File | Purpose | Where Located | +|------|---------|---------------| +| **API Base Classes** | ViewSet, Serializer base | `backend/igny8_core/api/base.py` | +| **Response Format** | Unified responses | `backend/igny8_core/api/response.py` | +| **Permissions** | Role-based access | `backend/igny8_core/api/permissions.py` | +| **Pagination** | List pagination | `backend/igny8_core/api/pagination.py` | +| **Root Router** | Route registration | `backend/igny8_core/urls.py` | +| **Settings** | Django config | `backend/igny8_core/settings.py` | +| **Auth Models** | User, Site, Sector | `backend/igny8_core/auth/models.py` | +| **API Client** | Frontend HTTP | `frontend/src/api/client.ts` | +| **Auth Store** | User state | `frontend/src/store/authStore.ts` | +| **Site Store** | Site/Sector context | `frontend/src/store/siteStore.ts` | +| **App Router** | Frontend routes | `frontend/src/App.tsx` | +| **Sidebar** | Navigation menu | `frontend/src/layout/Sidebar.tsx` | +| **Constants** | Global constants | `frontend/src/config/constants.ts` | +| **Validators** | Validation rules | `frontend/src/utils/validators.ts` | + +--- + +## Quick Checklists + +### Adding a New Column: Checklist + +- [ ] Add field to Backend Model (`models.py`) +- [ ] Run `makemigrations` +- [ ] Run `migrate` +- [ ] Add field to Backend Serializer (`serializers.py`) +- [ ] Add to ViewSet `filterset_fields` if filterable +- [ ] Add to ViewSet `ordering_fields` if sortable +- [ ] Add to Frontend Type (`types/models.ts`) +- [ ] Add to Frontend Component columns config +- [ ] Add to Frontend Table JSX rendering +- [ ] Test API response includes field +- [ ] Test frontend displays column +- [ ] Test filter/sort works (if applicable) + +### Adding a New Page: Checklist + +- [ ] Create new Page component (`pages/[Module]/[Page].tsx`) +- [ ] Create or update API hook (`api/[module].ts`) +- [ ] Add route to router (`App.tsx`) +- [ ] Add menu item to navigation (`layout/Sidebar.tsx`) +- [ ] Add types if needed (`types/models.ts`) +- [ ] Test page loads +- [ ] Test data fetches +- [ ] Test filters/search work +- [ ] Test pagination works +- [ ] Test error states + +### Adding an API Endpoint: Checklist + +- [ ] Define endpoint action in ViewSet (`views.py`) +- [ ] Use `@action(detail=...)` decorator +- [ ] Define response format using serializer +- [ ] Add permission check if needed +- [ ] Document docstring +- [ ] Test endpoint with API client +- [ ] Verify unified response format +- [ ] Create frontend API hook (`api/[module].ts`) +- [ ] Test from frontend component +- [ ] Verify error handling + +--- + +**End of Document** + +For questions on specific features, refer to: +- Architecture: `00-SYSTEM-ARCHITECTURE-MASTER-REFERENCE.md` +- API Details: `01-IGNY8-REST-API-COMPLETE-REFERENCE.md` +- Workflows: `02-PLANNER-WRITER-WORKFLOW-TECHNICAL-GUIDE.md` diff --git a/docs/igny8-app/IGNY8-APP-ARCHITECTURE.md b/old-docs/igny8-app/IGNY8-APP-ARCHITECTURE.md similarity index 100% rename from docs/igny8-app/IGNY8-APP-ARCHITECTURE.md rename to old-docs/igny8-app/IGNY8-APP-ARCHITECTURE.md diff --git a/old-docs/igny8-app/KEYWORDS-CLUSTERS-IDEAS-COMPLETE-MAPPING.md b/old-docs/igny8-app/KEYWORDS-CLUSTERS-IDEAS-COMPLETE-MAPPING.md new file mode 100644 index 00000000..d0c3e2aa --- /dev/null +++ b/old-docs/igny8-app/KEYWORDS-CLUSTERS-IDEAS-COMPLETE-MAPPING.md @@ -0,0 +1,263 @@ +# Keywords, Clusters & Ideas - Complete Field Mapping + +**Date:** December 3, 2025 + +--- + +## 📊 KEYWORDS - Complete Field Reference + +### Backend Model Fields +**File:** `backend/igny8_core/business/planning/models.py` + +| Field Name | Type | Default | Required | Choices | Notes | +|------------|------|---------|----------|---------|-------| +| `id` | Integer PK | Auto | ✅ | - | Primary key | +| `seed_keyword` | ForeignKey | - | ✅ | - | Links to SeedKeyword (PROTECT on delete) | +| `volume_override` | Integer | NULL | ❌ | - | Site-specific override (uses seed_keyword.volume if NULL) | +| `difficulty_override` | Integer | NULL | ❌ | - | Site-specific override (uses seed_keyword.difficulty if NULL) | +| `attribute_values` | JSONField | [] | ❌ | - | Optional metadata (product specs, modifiers) | +| `cluster` | ForeignKey | NULL | ❌ | - | Parent cluster (SET_NULL on delete) | +| `status` | CharField(50) | `pending` | ✅ | `pending`, `active`, `archived` | Keyword status | +| `site` | ForeignKey | - | ✅ | - | Owner site (inherited) | +| `sector` | ForeignKey | - | ✅ | - | Owner sector (inherited) | +| `account` | ForeignKey | - | ✅ | - | Owner account (inherited) | +| `created_at` | DateTime | Auto | ✅ | - | Auto-generated | +| `updated_at` | DateTime | Auto | ✅ | - | Auto-updated | + +### Frontend Table Columns +**File:** `frontend/src/config/pages/keywords.config.tsx` + +| Column Key | Label | Visible Default | Sortable | Render Notes | +|----------|--------|-----------------|----------|--------------| +| `keyword` | Keyword | ✅ Yes | ✅ Yes | From seed_keyword.keyword (links to Keywords page) | +| `sector_name` | Sector | Conditional* | ✅ Yes | Badge (blue) - shown when viewing all sectors | +| `volume` | Volume | ✅ Yes | ✅ Yes | Formatted as number (e.g., 1,250) | +| `cluster_name` | Cluster | ✅ Yes | ✅ Yes | Parent cluster name or "-" | +| `difficulty` | Difficulty | ✅ Yes | ✅ Yes | Badge (1-5): 1-2=green, 3=amber, 4-5=red | +| `intent` | Intent | ✅ Yes | ✅ Yes | Badge colors: Transactional/Commercial=green, Navigational=amber | +| `status` | Status | ✅ Yes | ✅ Yes | Badge: pending=amber, active=green, archived=red | +| `created_at` | Created | ✅ Yes | ✅ Yes | Relative date (e.g., "2 hours ago") | +| (Hidden by default) | | | | | +| `updated_at` | Updated | ❌ No | ✅ Yes | Relative date | + +### Frontend Filter Dropdown +**File:** `frontend/src/config/pages/keywords.config.tsx` (Lines 310-360) + +| Filter Key | Label | Type | Options | Dynamic | +|-----------|-------|------|---------|---------| +| `search` | Search | Text | N/A | - | +| `status` | Status | Select | `pending`, `active`, `archived` | ❌ No | +| `intent` | Intent | Select | `informational`, `navigational`, `transactional`, `commercial` | ❌ No | +| `difficulty` | Difficulty | Select | `1-5` with labels | ❌ No | +| `cluster` | Cluster | Select | Dynamic from database | ✅ Yes (loads clusters) | +| `volume` | Volume Range | Custom | Min/Max number inputs | ❌ No (range picker) | + +### Frontend Create/Edit Form +**File:** `frontend/src/config/pages/keywords.config.tsx` (Lines 560-586) + +| Field Key | Label | Type | Required | Default | Options | +|-----------|-------|------|----------|---------|---------| +| `seed_keyword_id` | Seed Keyword | Select | ✅ Yes | - | Dynamic from availableSeedKeywords | +| `volume_override` | Volume Override | Number | ❌ No | NULL | Numeric input (optional override) | +| `difficulty_override` | Difficulty Override | Number | ❌ No | NULL | Select 1-5 | +| `cluster_id` | Cluster | Select | ❌ No | NULL | Dynamic from clusters array | +| `status` | Status | Select | ✅ Yes | `pending` | `pending`, `active`, `archived` | + +--- + +## 📊 CLUSTERS - Complete Field Reference + +### Backend Model Fields +**File:** `backend/igny8_core/business/planning/models.py` + +| Field Name | Type | Default | Required | Choices | Notes | +|------------|------|---------|----------|---------|-------| +| `id` | Integer PK | Auto | ✅ | - | Primary key | +| `name` | CharField(255) | - | ✅ | - | Unique cluster name (unique=True) | +| `description` | TextField | NULL | ❌ | - | Optional cluster description | +| `keywords_count` | Integer | 0 | ✅ | - | Cached count of linked keywords | +| `ideas_count` | Integer | 0 | ✅ | - | Cached count of linked ideas | +| `volume` | Integer | 0 | ✅ | - | Cached total volume from keywords | +| `mapped_pages` | Integer | 0 | ✅ | - | Number of mapped pages | +| `status` | CharField(50) | `new` | ✅ | `new`, `idea`, `mapped` | Cluster status | +| `site` | ForeignKey | - | ✅ | - | Owner site (inherited) | +| `sector` | ForeignKey | - | ✅ | - | Owner sector (inherited) | +| `account` | ForeignKey | - | ✅ | - | Owner account (inherited) | +| `created_at` | DateTime | Auto | ✅ | - | Auto-generated | +| `updated_at` | DateTime | Auto | ✅ | - | Auto-updated | + +### Frontend Table Columns +**File:** `frontend/src/config/pages/clusters.config.tsx` + +| Column Key | Label | Visible Default | Sortable | Render Notes | +|----------|--------|-----------------|----------|--------------| +| `name` | Cluster Name | ✅ Yes | ✅ Yes | Link to cluster detail page | +| `sector_name` | Sector | Conditional* | ✅ Yes | Badge (blue) - shown when viewing all sectors | +| `keywords_count` | Keywords | ✅ Yes | ✅ Yes | Formatted as number (e.g., 45) | +| `ideas_count` | Ideas | ✅ Yes | ✅ Yes | Formatted as number (e.g., 12) | +| `volume` | Volume | ✅ Yes | ✅ Yes | Formatted as number (e.g., 5,280) | +| `difficulty` | Difficulty | ✅ Yes | ✅ Yes | Badge (1-5): 1-2=green, 3=amber, 4-5=red | +| `content_count` | Content | ✅ Yes | ✅ Yes | Formatted as number | +| `status` | Status | ✅ Yes | ✅ Yes | Badge: new=amber, idea=blue, mapped=green | +| `created_at` | Created | ✅ Yes | ✅ Yes | Relative date | +| (Hidden by default) | | | | | +| `description` | Description | ❌ No | ❌ No | Text truncated to 250px | +| `mapped_pages` | Mapped Pages | ❌ No | ✅ Yes | Formatted number | +| `updated_at` | Updated | ❌ No | ✅ Yes | Relative date | + +### Frontend Filter Dropdown +**File:** `frontend/src/config/pages/clusters.config.tsx` (Lines 240-290) + +| Filter Key | Label | Type | Options | Dynamic | +|-----------|-------|------|---------|---------| +| `search` | Search | Text | N/A | - | +| `status` | Status | Select | `new`, `idea`, `mapped` | ❌ No | +| `difficulty` | Difficulty | Select | `1-5` with labels | ❌ No | +| `volume` | Volume Range | Custom | Min/Max number inputs | ❌ No (range picker) | + +### Frontend Create/Edit Form +**File:** `frontend/src/config/pages/clusters.config.tsx` (Lines 405-418) + +| Field Key | Label | Type | Required | Default | Options | +|-----------|-------|------|----------|---------|---------| +| `name` | Cluster Name | Text | ✅ Yes | - | Text input (placeholder: "Enter cluster name") | +| `description` | Description | Textarea | ❌ No | NULL | Textarea (placeholder: "Enter cluster description") | +| `status` | Status | Select | ✅ Yes | `new` | `new`, `idea`, `mapped` | + +--- + +## 📊 CONTENT IDEAS - Complete Field Reference + +### Backend Model Fields +**File:** `backend/igny8_core/business/planning/models.py` + +| Field Name | Type | Default | Required | Choices | Notes | +|------------|------|---------|----------|---------|-------| +| `id` | Integer PK | Auto | ✅ | - | Primary key | +| `idea_title` | CharField(255) | - | ✅ | - | Content idea title | +| `description` | TextField | NULL | ❌ | - | Content outline/description | +| `target_keywords` | CharField(500) | '' | ❌ | - | Comma-separated keywords (legacy) | +| `keyword_objects` | M2M(Keywords) | - | ❌ | - | Individual keywords linked to idea | +| `keyword_cluster` | ForeignKey(Clusters) | NULL | ❌ | - | Parent cluster (SET_NULL on delete) | +| `status` | CharField(50) | `new` | ✅ | `new`, `scheduled`, `completed`, `published` | Idea workflow status | +| `estimated_word_count` | Integer | 1000 | ✅ | - | Target article length | +| `content_type` | CharField(50) | `post` | ✅ | `post`, `page`, `product`, `taxonomy` | Content type | +| `content_structure` | CharField(50) | `article` | ✅ | See structures below | Content format/structure | +| `site` | ForeignKey | - | ✅ | - | Owner site (inherited) | +| `sector` | ForeignKey | - | ✅ | - | Owner sector (inherited) | +| `account` | ForeignKey | - | ✅ | - | Owner account (inherited) | +| `created_at` | DateTime | Auto | ✅ | - | Auto-generated | +| `updated_at` | DateTime | Auto | ✅ | - | Auto-updated | + +**Content Structure Choices (based on content_type):** +- **Post:** `article`, `guide`, `comparison`, `review`, `listicle` +- **Page:** `landing_page`, `business_page`, `service_page`, `general`, `cluster_hub` +- **Product:** `product_page` +- **Taxonomy:** `category_archive`, `tag_archive`, `attribute_archive` + +### Frontend Table Columns +**File:** `frontend/src/config/pages/ideas.config.tsx` + +| Column Key | Label | Visible Default | Sortable | Render Notes | +|----------|--------|-----------------|----------|--------------| +| `idea_title` | Title | ✅ Yes | ✅ Yes | Expandable (shows description) | +| `sector_name` | Sector | Conditional* | ✅ Yes | Badge (blue) - shown when viewing all sectors | +| `content_structure` | Structure | ✅ Yes | ✅ Yes | Badge (purple): article, guide, guide, etc. | +| `content_type` | Type | ✅ Yes | ✅ Yes | Badge (blue): post, page, product, taxonomy | +| `target_keywords` | Target Keywords | ✅ Yes | ❌ No | Text truncated to 250px | +| `keyword_cluster_name` | Cluster | ✅ Yes | ✅ Yes | Parent cluster name or "-" | +| `status` | Status | ✅ Yes | ✅ Yes | Badge: new=amber, scheduled=blue, completed=blue, published=green | +| `estimated_word_count` | Words | ✅ Yes | ✅ Yes | Formatted as number (e.g., 1,500) | +| `created_at` | Created | ✅ Yes | ✅ Yes | Relative date | +| (Hidden by default) | | | | | +| `updated_at` | Updated | ❌ No | ✅ Yes | Relative date | + +### Frontend Filter Dropdown +**File:** `frontend/src/config/pages/ideas.config.tsx` (Lines 218-270) + +| Filter Key | Label | Type | Options | Dynamic | +|-----------|-------|------|---------|---------| +| `search` | Search | Text | N/A | - | +| `status` | Status | Select | `new`, `scheduled`, `completed`, `published` | ❌ No | +| `content_structure` | Structure | Select | article, guide, comparison, etc. (all 13 options) | ❌ No | +| `content_type` | Type | Select | `post`, `page`, `product`, `taxonomy` | ❌ No | +| `cluster` | Cluster | Select | Dynamic from database | ✅ Yes (loads clusters) | + +### Frontend Create/Edit Form +**File:** `frontend/src/config/pages/ideas.config.tsx` (Lines 372-417) + +| Field Key | Label | Type | Required | Default | Options | +|-----------|-------|------|----------|---------|---------| +| `idea_title` | Title | Text | ✅ Yes | - | Text input (placeholder: "Enter idea title") | +| `description` | Description | Textarea | ❌ No | NULL | Textarea (placeholder: "Enter content outline") | +| `content_type` | Content Type | Select | ✅ Yes | `post` | `post`, `page`, `product`, `taxonomy` | +| `content_structure` | Content Structure | Select | ✅ Yes | `article` | 13 structure options (depends on content_type) | +| `target_keywords` | Target Keywords | Text | ❌ No | NULL | Text input (comma-separated) | +| `keyword_cluster_id` | Cluster | Select | ❌ No | NULL | Dynamic from clusters array | +| `status` | Status | Select | ✅ Yes | `new` | `new`, `scheduled`, `completed`, `published` | +| `estimated_word_count` | Word Count | Number | ❌ No | 1000 | Numeric input | + +--- + +## 📝 SUMMARY COMPARISON + +### Status Fields +| Module | Backend Default | Backend Choices | Frontend Form Default | Frontend Form Choices | +|--------|-----------------|-----------------|----------------------|----------------------| +| **Keywords** | `pending` | pending, active, archived | `pending` | pending, active, archived | +| **Clusters** | `new` | new, idea, mapped | `new` | new, idea, mapped | +| **Ideas** | `new` | new, scheduled, completed, published | `new` | new, scheduled, completed, published | + +### Required Fields (Must be filled) +| Module | Required Fields | +|--------|-----------------| +| **Keywords** | seed_keyword_id, status, site, sector, account | +| **Clusters** | name, status, site, sector, account | +| **Ideas** | idea_title, status, content_type, content_structure, estimated_word_count, site, sector, account | + +### Optional Fields +| Module | Optional Fields | +|--------|-----------------| +| **Keywords** | volume_override, difficulty_override, attribute_values, cluster_id | +| **Clusters** | description | +| **Ideas** | description, target_keywords, keyword_objects, keyword_cluster_id | + +### Dynamic Dropdowns (Loaded from DB) +| Module | Filter | Form | +|--------|--------|------| +| **Keywords** | cluster (dropdown) | seed_keyword_id (all available), cluster_id (all clusters for sector) | +| **Clusters** | - | - | +| **Ideas** | cluster (dropdown) | keyword_cluster_id (all clusters for sector) | + +### Visible-by-Default Table Columns +| Module | Count | Primary Columns | +|--------|-------|-----------------| +| **Keywords** | 9 | keyword, volume, cluster, difficulty, intent, status, created_at | +| **Clusters** | 11 | name, keywords_count, ideas_count, volume, difficulty, content_count, status, created_at | +| **Ideas** | 10 | idea_title, content_structure, content_type, target_keywords, cluster, status, word_count, created_at | + +--- + +## 🔑 Key Differences + +### Keywords +- **Uses:** SeedKeyword (global pool) - one keyword per site/sector +- **Overrideable:** volume, difficulty (site-specific) +- **Links to:** Clusters (via cluster FK) +- **Status:** pending (awaiting cluster), active (clustered), archived + +### Clusters +- **Type:** Pure topic clusters (semantic groupings) +- **Auto-updated:** keywords_count, ideas_count, volume (cached from keywords) +- **Status:** new (no ideas), idea (has ideas), mapped (has content) +- **No overrides:** All values are cached/calculated + +### Ideas +- **Type:** Content concepts ready for production +- **Links:** Cluster (required for workflow), Keywords (optional M2M) +- **Customizable:** content_type, content_structure, word_count +- **Status:** new → scheduled (queued to writer) → completed (content generated) → published + +--- + +**END OF COMPLETE FIELD MAPPING** diff --git a/old-docs/igny8-app/TAXONOMY/TAXONOMY-RELATIONSHIP-DIAGRAM.md b/old-docs/igny8-app/TAXONOMY/TAXONOMY-RELATIONSHIP-DIAGRAM.md new file mode 100644 index 00000000..46ae0326 --- /dev/null +++ b/old-docs/igny8-app/TAXONOMY/TAXONOMY-RELATIONSHIP-DIAGRAM.md @@ -0,0 +1,258 @@ +# Content Taxonomy Relationship Diagram + +## Current Architecture (Simplified) + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ IGNY8 Content System │ +└─────────────────────────────────────────────────────────────────────┘ + +┌──────────────────────────────────────────────────────────────────────┐ +│ Multi-Tenant Hierarchy │ +│ │ +│ Account ──┬── Site ──┬── Sector ──┬── Keywords │ +│ │ │ ├── Clusters │ +│ │ │ ├── Ideas │ +│ │ │ ├── Tasks │ +│ │ │ ├── Content │ +│ │ │ └── ContentTaxonomy │ +│ │ └── Sector 2 │ +│ └── Site 2 │ +└──────────────────────────────────────────────────────────────────────┘ + + +┌──────────────────────────────────────────────────────────────────────┐ +│ Planner → Writer → Publisher Workflow │ +│ │ +│ Phase 1-3: PLANNER │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ Keywords │ ──> │ Clusters │ ──> │ Ideas │ │ +│ └──────────┘ └──────────┘ └──────────┘ │ +│ │ │ │ │ +│ │ │ │ │ +│ Phase 4: WRITER │ +│ │ │ │ │ +│ └─────────────────┴─────────────────┘ │ +│ │ │ +│ v │ +│ ┌──────────┐ ┌──────────┐ │ +│ │ Tasks │ ──> │ Content │ │ +│ └──────────┘ └──────────┘ │ +│ │ │ +│ │ │ +│ Phase 5: PUBLISHER │ │ +│ v │ +│ ┌─────────────┐ │ +│ │ WordPress │ │ +│ │ Shopify │ │ +│ │ Sites │ │ +│ └─────────────┘ │ +└──────────────────────────────────────────────────────────────────────┘ + + +┌──────────────────────────────────────────────────────────────────────┐ +│ Content ↔ Taxonomy Relationship (Many-to-Many) │ +└──────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────┐ +│ Content Model │ +│─────────────────────────────────│ +│ PK: id │ +│ FK: site_id ──────┐ │ +│ FK: sector_id │ │ +│ FK: cluster_id │ │ +│ │ │ +│ title │ │ +│ content_html │ │ +│ word_count │ │ +│ meta_title │ │ +│ meta_description │ │ +│ │ │ +│ content_type │ │ +│ content_structure │ │ +│ │ │ +│ external_id │ │ +│ external_url │ │ +│ sync_status │ │ +│ │ │ +│ source │ │ +│ status │ │ +└───────────┬───────┘ │ + │ │ + │ Many-to-Many │ + │ (via ContentTaxonomyRelation) + │ │ + v │ +┌─────────────────────────────────┐│ +│ ContentTaxonomyRelation Model ││ +│─────────────────────────────────││ +│ PK: id ││ +│ FK: content_id ──────────────────┘ +│ FK: taxonomy_id ────────────┐ +│ │ +│ created_at │ +│ updated_at │ +│ │ +│ UNIQUE(content, taxonomy) │ +└─────────────────────────────┘ + │ + │ + v + ┌─────────────────────────────────┐ + │ ContentTaxonomy Model │ + │─────────────────────────────────│ + │ PK: id │ + │ FK: site_id ─────┐ │ + │ FK: sector_id │ │ + │ │ │ + │ name │ │ + │ slug │ │ + │ taxonomy_type │ ◄─── "category" or "tag" + │ description │ │ + │ count │ │ + │ │ │ + │ WordPress Sync: │ │ + │ external_taxonomy│ ◄─── "category", "post_tag" + │ external_id │ ◄─── WordPress term_id + │ │ │ + │ metadata (JSON) │ │ + └──────────────────┘ │ + │ │ + │ │ + UNIQUE(site, slug, taxonomy_type)│ + UNIQUE(site, external_id, external_taxonomy) + │ │ + └──────────────┘ + + +┌──────────────────────────────────────────────────────────────────────┐ +│ Usage Example │ +└──────────────────────────────────────────────────────────────────────┘ + +# Get all categories for a content piece +categories = content.taxonomy_terms.filter(taxonomy_type='category') + +# Get all tags for a content piece +tags = content.taxonomy_terms.filter(taxonomy_type='tag') + +# Add a category to content +tech_category = ContentTaxonomy.objects.get(name='Technology') +content.taxonomy_terms.add(tech_category) + +# Get all content with a specific tag +tutorial_tag = ContentTaxonomy.objects.get(name='Tutorial') +contents = tutorial_tag.contents.all() + +# WordPress publishing (automatic) +wp_categories = [term.name for term in content.taxonomy_terms.filter(taxonomy_type='category')] +wp_tags = [term.name for term in content.taxonomy_terms.filter(taxonomy_type='tag')] + + +┌──────────────────────────────────────────────────────────────────────┐ +│ WordPress Integration (Bidirectional Sync) │ +└──────────────────────────────────────────────────────────────────────┘ + +IGNY8 → WordPress: +───────────────── +1. Content created in IGNY8 +2. Categories/tags assigned via taxonomy_terms M2M +3. Publishing task created +4. wordpress_publishing.py extracts: + - categories = content.taxonomy_terms.filter(taxonomy_type='category') + - tags = content.taxonomy_terms.filter(taxonomy_type='tag') +5. REST API creates WordPress post with terms +6. external_id saved back to Content model +7. Log: [5-homeg8.com] [POST] Published: "Article Title" (ID: 123) + +WordPress → IGNY8: +───────────────── +1. WordPress plugin detects post update +2. REST API sends post data + terms to IGNY8 +3. Content updated/created with external_id +4. ContentTaxonomy created/updated with external_id +5. ContentTaxonomyRelation created linking them +6. Log: [5-homeg8.com] [SYNC] Imported: "Article Title" + + +┌──────────────────────────────────────────────────────────────────────┐ +│ Database Tables Summary │ +└──────────────────────────────────────────────────────────────────────┘ + +igny8_content +├─ id (PK) +├─ site_id (FK → sites) +├─ sector_id (FK → sectors) +├─ cluster_id (FK → clusters) +├─ title, content_html, word_count +├─ meta_title, meta_description, keywords +├─ content_type, content_structure +├─ external_id, external_url, sync_status +└─ source, status, timestamps + +igny8_content_taxonomy_relations (Through Table) +├─ id (PK) +├─ content_id (FK → igny8_content) +├─ taxonomy_id (FK → igny8_content_taxonomy_terms) +└─ timestamps + UNIQUE(content_id, taxonomy_id) + +igny8_content_taxonomy_terms +├─ id (PK) +├─ site_id (FK → sites) +├─ sector_id (FK → sectors) +├─ name, slug +├─ taxonomy_type ('category' | 'tag') +├─ description, count +├─ external_taxonomy ('category' | 'post_tag') +├─ external_id (WordPress term_id) +├─ metadata (JSON) +└─ timestamps + UNIQUE(site_id, slug, taxonomy_type) + UNIQUE(site_id, external_id, external_taxonomy) + + +┌──────────────────────────────────────────────────────────────────────┐ +│ Before vs After Comparison │ +└──────────────────────────────────────────────────────────────────────┘ + +BEFORE (Complex - with SiteBlueprint): +──────────────────────────────────── +SiteBlueprint ──┬── SiteBlueprintCluster ──> Clusters + ├── SiteBlueprintTaxonomy ──> ContentTaxonomy + └── PageBlueprint ──> Content + +Content ──> ContentTaxonomyMap ──> ContentTaxonomy + (separate table with FK) + +PublishingRecord ──┬── content_id + └── site_blueprint_id + +DeploymentRecord ──┬── content_id + └── site_blueprint_id + + +AFTER (Simple - SiteBlueprint Removed): +────────────────────────────────────── +Keywords ──> Clusters ──> Ideas ──> Tasks ──> Content + +Content ↔ ContentTaxonomy + (M2M via ContentTaxonomyRelation) + +PublishingRecord ──> content_id + +DeploymentRecord ──> content_id + + +┌──────────────────────────────────────────────────────────────────────┐ +│ Key Benefits │ +└──────────────────────────────────────────────────────────────────────┘ + +✅ Simpler architecture - removed 4 models +✅ Direct M2M relationship - easier to query +✅ Less database joins - better performance +✅ Clear taxonomy model in Django admin +✅ WordPress sync unchanged - still works perfectly +✅ Planner-Writer-Publisher workflow intact +✅ Multi-tenant security maintained +✅ No circular dependencies +✅ Clean codebase - no legacy blueprint code diff --git a/old-docs/igny8-app/app-packaging-backaup-plan.md b/old-docs/igny8-app/app-packaging-backaup-plan.md new file mode 100644 index 00000000..be793b98 --- /dev/null +++ b/old-docs/igny8-app/app-packaging-backaup-plan.md @@ -0,0 +1,237 @@ +bluesalman: gothrough readme and tell me which folders/files inside backeedn adn frontedn folders are acutaly part of igny8 app, and which folders are part of some tech stack which will be enever modifed by app only isntaleld when tech stack isntaleled + +GitHub Copilot: I'll create a short plan, then list the backend and frontend directories to inspect their contents. + +Created 3 todos + +Now I'll list both directories (backend and frontend) to collect the items to classify. + +Read [](file:///data/app/igny8/backend) + +Read [](file:///data/app/igny8/frontend) + +Marking plan steps completed and finishing classification. + +Completed: *List backend files* (1/3) + +Progress: I scanned backend and frontend and classified items below. + +**Backend — Classified** +- **App (igny8 application core):** + - `igny8_core/`: The Django project / app code (settings, apps, views, models, tasks). This is the main application source you will modify. + - `manage.py`: Django management entrypoint — part of the app. + - `migrations/`: Django DB migrations — app schema changes (source-controlled). + - `staticfiles/`: Collected static assets (app assets). May be generated but belongs to the app. + - `requirements.txt`: Dependency manifest for the app (used to install the Python packages the app needs). Not runtime third-party code, but part of the app repo and should be version-controlled. + +- **Repo tooling / app-maintenance scripts (part of the repo; maintained by your team):** + - `check_api_response.py`, `check_recent_keywords.py`, `cleanup_structure_categories.py`, `diagnose_generate_content.py`, `inject_test_data.py`, `sync_idea_status.py`, `test_tags_categories.py`, `verify_*.py`, `fix_*` scripts, `final_verify.py`, `deploy_automation.sh`, `example_csv/`: These are maintenance, verification, or migration scripts that are part of the repository and used by developers/ops. They are not third-party code but are repo-level tooling you may edit. + +- **Tech stack / infra artifacts (should not be modified by the app runtime; installed or produced by infra):** + - `.venv/`: Virtual environment — environment artifact; not part of app source and typically excluded from commits. + - `Dockerfile`: Container build file — infra/build config (used by ops or CI). + - `backup_postgres_20251120_232816.sql`, `db_backup_20251120_232646.sqlite3`, `rename_fields_migration.sql`: Database backups / SQL dumps — artifacts, not app source. + - `celerybeat-schedule`: Celery scheduler state file — runtime artifact. + - `logs/`: Runtime logs — generated artifacts. + - Any large generated files (e.g., DB dumps) are infra artifacts and shouldn't be edited as source. + +Notes: +- Some items like `deploy_automation.sh` or `fix_*` scripts are operational but live in the repo — treat them as repository tooling (editable by you), whereas backups, `.venv`, and docker images/configs are infra artifacts. + +**Frontend — Classified** +- **App (igny8 frontend source / code you will modify):** + - `src/`: Application source (React/Vue/Svelte/TSX/etc.) — primary frontend code to edit. + - `public/`, `index.html`, `marketing.html`, `banner.png`: Static assets and HTML entrypoints — part of the app. + - `package.json`, `package-lock.json`: NPM manifests — define packages the frontend needs (repo-managed). + - `eslint.config.js`, `postcss.config.js`, `vite.config.ts`, `vitest.config.ts`, `tsconfig*.json`: Build and tooling config that belong to the frontend source and are edited as part of app development. + - README.md, `DESIGN_SYSTEM.md`, `MIGRATION_GUIDE.md`: Documentation for the frontend — repo docs. + +- **Tech stack / infra artifacts (installed/generated by stack — do not edit at runtime):** + - `node_modules/`: Installed dependencies — generated by `npm` / `pnpm` / `yarn`; should not be manually edited and typically ignored by VCS. + - `dist/`: Built production bundle — generated artifact (output of the build). + - `Caddyfile`, `Caddyfile.marketing`, `Dockerfile`, `Dockerfile.dev`, `Dockerfile.marketing`, `Dockerfile.marketing.dev`: Webserver and container build configs — deployment/infra files. They belong to ops/build but do live in the repo; treat as infra configuration, not application logic. + - `LICENSE.md`, other top-level docs: docs, not runtime app code. + +Notes: +- Build-tool config files (e.g., `vite.config.ts`, `tsconfig.json`) are part of the app repository and are changed by developers. By contrast, `node_modules` and `dist` are installed/generated by the tech stack and should not be edited directly. + +Quick summary (one-line): +- Edit: igny8_core, `manage.py`, migrations, src, public, `package.json`, and similar source/config files — these are part of the igny8 app. +- Don't manually edit: runtime/generated/installed artifacts like `.venv/`, `node_modules/`, `dist/`, DB backups, `celerybeat-schedule`, and use `Dockerfile`/`Caddyfile` only as infra config (ops-managed). + +Would you like: +- a) A file added to the repo (e.g., `ARCHITECTURE.md` or `IGNY8-CONTENTS.md`) that labels each top-level item as "app" vs "infra", or +- b) I can produce a JSON/CSV mapping of items for automation (useful for CI), or +- c) Walk through any specific file/folder you want deeper inspection of? + +Which option do you prefer? + +bluesalman: ok now cehck teh strcutre of /data adn recrussively all its fodlers including igny8/ +waht can we do if we want to make igny8 folder as the only full app foodler requried to move to any new location, +and jsut install the portainer as current envirnpmaent and isntall teh app package on nwe locatink build the package and it runs there, + +teh curretn setup isnto standrd as it ocntians some node )moduels dist and env, +venv i know can be removed completely + +so waht do you think + +GitHub Copilot: I'll make a short plan, then list data and the key subfolders (app, igny8, and their important children) so I can analyze what to keep, remove, or package. + +Created 4 todos + +Read [](file:///data) + +Updating the plan: mark the scan step done (noting data access blocked) and keep other steps ready. Then I'll provide analysis, exact commands, Docker and non-Docker packaging options, and a sample `docker-compose.yml` and `install.sh`. + +bluesalman: waht happend you are stucl + +GitHub Copilot: I'll create a short todo plan, then list data, app, and igny8 to inspect contents before proposing a packaging strategy. + +Created 4 todos + +Read [](file:///data) + +Now I'll run a quick directory size and shallow tree under the workspace `igny8/` so we can see which folders (like `node_modules`, `.venv`, `dist`) are large and where they are. + +Ran terminal command: cd /data/app/igny8 && echo 'Top-level sizes:' && du -sh * 2>/dev/null | sort -h && echo && echo 'Directory tree (depth 3):' && find . -maxdepth 3 -type d | sed 's|^\./||' | sort + +Updating the todo list: mark the listing steps done and the analysis/packaging step in-progress. + +Starting: *Analyze and propose packaging* (4/4) + +Progress update: I inspected the workspace `igny8/` (sizes and a shallow tree). Next I analyzed which files to keep/exclude and drafted packaging, cleanup commands, and a sample `docker-compose.yml` plus a `prepare_package.sh` plan. Below are clear findings and actionable steps. + +**Findings** +- **Large/generated artifacts present:** + - .venv — virtualenv (can be removed for packaging). + - node_modules, node_modules — installed npm dependencies (do not include in package). + - dist — built frontend assets (can be produced during build; you can include or build on deploy). + - DB dumps and scheduler/state files appear under backend (e.g., `*.sql`, `*.sqlite3`, `celerybeat-schedule`) — these are artifacts and should not be bundled as app source. +- **App source & config (keep these):** + - igny8_core, manage.py, migrations, requirements.txt — Django app and deps manifest. + - src, public, package.json and build config files — frontend source and build config. + - `Dockerfile` / `Dockerfile.*` and `Caddyfile` — infra config that should remain in the repo (useful for containerization). +- **Repo tooling and docs:** `deploy_automation.sh`, `verify_*.py`, docs, README.md — keep in repo; useful for ops/dev. + +**Goal** +Make `igny8/` a single, relocatable application folder that you can move to a new host, then deploy via Portainer (or Docker Compose) without shipping generated artifacts like `node_modules`, `.venv`, or large backups. + +Recommendations and exact steps + +- **Strategy (recommended): Docker image + single `docker-compose.yml` stack** + - Keep only source, config, Dockerfiles and manifests in the app folder. + - Exclude generated artifacts from the package (`.venv`, `node_modules`, `dist`, backups, logs). + - Provide a `docker-compose.yml` that builds the backend and frontend images from the repo and defines services: `db` (Postgres), `redis`, backend (Gunicorn/Django), `worker` (Celery), and frontend (built and served by Caddy or served as static in a small nginx/caddy image). + - Deploy via Portainer by uploading the `docker-compose.yml` (Portainer can build images from contexts if configured), or pre-build images, push to container registry, and use compose with image references. + +- **Alternative:** Tarball + install script that runs `docker compose build` and `docker compose up -d` on the new host. + +- **Safety note:** Always create a backup or branch of the repo before mass-deleting artifacts. + +Exact commands to produce a clean package (run from igny8): + +```bash +# make a copy first (safe) +cp -a /data/app/igny8 /tmp/igny8-package-source + +# inside the copy, remove generated artifacts +cd /tmp/igny8-package-source +rm -rf backend/.venv +rm -rf backend/*.sql backend/*.sqlite3 backend/celerybeat-schedule backend/logs || true +rm -rf frontend/node_modules sites/node_modules +rm -rf frontend/dist +# optional: remove any large backups in root +rm -f backup_*.sql || true + +# create archive +tar -czf /tmp/igny8-clean.tgz . + +# verify size +ls -lh /tmp/igny8-clean.tgz +``` + +Suggested `.dockerignore` (place in repo root) to keep image builds small: + +``` +backend/.venv +frontend/node_modules +sites/node_modules +frontend/dist +*.sql +*.sqlite3 +logs/ +*.log +.git +node_modules +``` + +Sample minimal `docker-compose.yml` (adapt to your app's env vars and Dockerfiles): + +```yaml +version: '3.8' +services: + db: + image: postgres:15 + environment: + POSTGRES_DB: igny8 + POSTGRES_USER: igny8 + POSTGRES_PASSWORD: changeme + volumes: + - db_data:/var/lib/postgresql/data + + redis: + image: redis:7 + command: redis-server --appendonly yes + + backend: + build: + context: ./backend + dockerfile: Dockerfile + depends_on: + - db + - redis + environment: + DATABASE_URL: postgres://igny8:changeme@db:5432/igny8 + REDIS_URL: redis://redis:6379/0 + ports: + - "8000:8000" + volumes: + - static_volume:/app/staticfiles + + worker: + build: + context: ./backend + dockerfile: Dockerfile + command: celery -A igny8_core worker --loglevel=info + depends_on: + - redis + - db + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + ports: + - "3000:80" + depends_on: + - backend + +volumes: + db_data: + static_volume: +``` + +Notes: +- The Dockerfile should install Python deps from `requirements.txt`, collect static, run migrations in a start-up script, and run Gunicorn. If not present, we can produce a small `Dockerfile`. +- The Dockerfile should `npm ci && npm run build` and copy `dist/` into a small static-server image (Caddy or nginx). If you prefer to serve frontend via the backend/CDN, build and copy the static files into backend static volume. + +What I'd do next (I can do these for you) +- Option A (recommended): Create `docker-compose.yml` and `prepare_package.sh` in the repo (I can add both files), plus `.dockerignore` and a minimal Dockerfile / Dockerfile if missing. Then test `docker compose up --build` locally. +- Option B: Create an archive script only (if you prefer to handle Dockerfiles yourself). + +Would you like me to: +- **(1)** Create the `prepare_package.sh`, `.dockerignore`, and a `docker-compose.yml` in the repository and test the compose up here, or +- **(2)** Only produce the files and commands you can run on your target host (no changes to repo), or +- **(3)** Generate a simple Dockerfile and Dockerfile for a fully self-contained build? + +Pick an option and I’ll implement it. \ No newline at end of file diff --git a/docs/igny8-app/status-related-temporary/STATUS-IMPLEMENTATION-TABLES.md b/old-docs/igny8-app/status-related-temporary/STATUS-IMPLEMENTATION-TABLES.md similarity index 100% rename from docs/igny8-app/status-related-temporary/STATUS-IMPLEMENTATION-TABLES.md rename to old-docs/igny8-app/status-related-temporary/STATUS-IMPLEMENTATION-TABLES.md diff --git a/docs/igny8-app/status-related-temporary/status-dependency.md b/old-docs/igny8-app/status-related-temporary/status-dependency.md similarity index 100% rename from docs/igny8-app/status-related-temporary/status-dependency.md rename to old-docs/igny8-app/status-related-temporary/status-dependency.md diff --git a/old-docs/tech-stack/00-SYSTEM-ARCHITECTURE-MASTER-REFERENCE.md b/old-docs/tech-stack/00-SYSTEM-ARCHITECTURE-MASTER-REFERENCE.md new file mode 100644 index 00000000..d3f06040 --- /dev/null +++ b/old-docs/tech-stack/00-SYSTEM-ARCHITECTURE-MASTER-REFERENCE.md @@ -0,0 +1,1984 @@ +# IGNY8 System Master Reference + +**Version:** 1.0.0 +**Last Updated:** November 24, 2025 +**Purpose:** Complete system architecture and navigation reference for the IGNY8 SaaS platform and WordPress integration plugin. + +--- + +## Table of Contents + +1. [System Overview](#system-overview) +2. [Architecture](#architecture) +3. [Backend Deep Dive](#backend-deep-dive) +4. [Frontend Deep Dive](#frontend-deep-dive) +5. [WordPress Plugin Deep Dive](#wordpress-plugin-deep-dive) +6. [Integration Architecture](#integration-architecture) +7. [AI Framework](#ai-framework) +8. [Data Models](#data-models) +9. [API Reference](#api-reference) +10. [Workflows](#workflows) +11. [Developer Navigation Map](#developer-navigation-map) +12. [Troubleshooting Map](#troubleshooting-map) + +--- + +## System Overview + +IGNY8 is a full-stack AI-powered SEO content management platform consisting of three main components: + +1. **IGNY8 SaaS Application** - Multi-tenant Django + React application +2. **WordPress Bridge Plugin** - Bidirectional sync between WordPress and IGNY8 +3. **Sites Renderer** - Public-facing site deployment system + +### Core Capabilities + +| Capability | Implementation | Status | +|------------|----------------|--------| +| **Multi-Tenancy** | Account → Site → Sector hierarchy | ✅ Live | +| **Keyword Management** | Planner module (Keywords, Clusters, Ideas) | ✅ Live | +| **AI Content Generation** | Writer module with OpenAI integration | ✅ Live | +| **Image Generation** | AI-powered via DALL-E and Runware | ✅ Live | +| **WordPress Publishing** | Bidirectional sync via REST API | ✅ Live | +| **Internal Linking** | Linker module for SEO optimization | ✅ Live | +| **Content Optimization** | Optimizer module for scoring | ✅ Live | +| **Automation** | Scheduled tasks and rules | ✅ Live | +| **Credit System** | Usage-based billing | ✅ Live | + +--- + +## Architecture + +### High-Level System Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ User Layer │ +├─────────────────────────────────────────────────────────────────┤ +│ React Frontend (app.igny8.com) │ WordPress Site (client) │ +│ - Vite 6.1.0 + React 19 │ - WP Bridge Plugin │ +│ - Zustand State Management │ - REST API Endpoints │ +│ - React Router v7 │ - Bidirectional Sync │ +└─────────────────────┬───────────────────────────┬───────────────┘ + │ │ + ▼ ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ API Gateway (Django) │ +├─────────────────────────────────────────────────────────────────┤ +│ - JWT Authentication │ +│ - Unified Response Format (success, data, message, request_id) │ +│ - Rate Limiting & Throttling │ +│ - Account Context Middleware │ +└─────────────────────┬───────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Business Logic Layer │ +├─────────────────────────────────────────────────────────────────┤ +│ Modules (ViewSets) │ Business Logic (Services) │ +│ ─────────────────────────── │ ──────────────────────────── │ +│ • Planner │ • Planning Services │ +│ • Writer │ • Content Services │ +│ • Linker │ • Linking Services │ +│ • Optimizer │ • Optimization Services │ +│ • Publisher │ • Publishing Services │ +│ • Automation │ • Automation Services │ +│ • Integration │ • Integration Services │ +│ • System │ • System Settings │ +│ • Billing │ • Credit Services │ +└─────────────────────┬───────────────────┬───────────────────────┘ + │ │ + ▼ ▼ +┌──────────────────────────────┐ ┌──────────────────────────────┐ +│ AI Framework (Celery) │ │ Database (PostgreSQL) │ +├──────────────────────────────┤ ├──────────────────────────────┤ +│ • AIEngine │ │ • Account Isolation │ +│ • AICore │ │ • Site/Sector Scoping │ +│ • AI Functions Registry │ │ • Relational Models │ +│ • Progress Tracking │ │ • JSON Fields for Config │ +│ • Cost Tracking │ │ • Indexed Queries │ +└──────────────────────────────┘ └──────────────────────────────┘ +``` + +### Data Hierarchy + +``` +Account (Tenant) +├── Users (with roles) +├── Plan & Subscription +├── Credit Balance +└── Sites (1-N based on plan) + ├── Industry (optional) + ├── WordPress Integration (SiteIntegration) + └── Sectors (1-5 per site) + ├── Keywords + ├── Clusters + ├── Content Ideas + ├── Tasks + ├── Content + └── Images +``` + +--- + +## Backend Deep Dive + +### Technology Stack + +- **Framework:** Django 5.2+ +- **API:** Django REST Framework (DRF) with drf-spectacular +- **Database:** PostgreSQL +- **Task Queue:** Celery 5.3+ with Redis broker +- **Authentication:** JWT (PyJWT 2.8+) +- **AI:** OpenAI API, Runware API + +### Project Structure + +``` +backend/ +├── igny8_core/ # Django project root +│ ├── settings.py # Django configuration +│ ├── urls.py # Root URL routing +│ ├── celery.py # Celery configuration +│ ├── wsgi.py # WSGI entry point +│ ├── asgi.py # ASGI entry point (future) +│ │ +│ ├── auth/ # Multi-tenancy & authentication +│ │ ├── models.py # Account, User, Plan, Site, Sector +│ │ ├── views.py # Auth ViewSets +│ │ ├── serializers.py # Auth serializers +│ │ ├── middleware.py # AccountContextMiddleware +│ │ └── urls.py # /api/v1/auth/ routes +│ │ +│ ├── modules/ # Feature modules (ViewSets) +│ │ ├── planner/ # Keywords, Clusters, Ideas +│ │ │ ├── views.py # KeywordViewSet, ClusterViewSet, ContentIdeasViewSet +│ │ │ ├── serializers.py # Serializers +│ │ │ ├── urls.py # /api/v1/planner/ routes +│ │ │ └── apps.py # App config +│ │ │ +│ │ ├── writer/ # Tasks, Content, Images +│ │ │ ├── views.py # TasksViewSet, ContentViewSet, ImagesViewSet +│ │ │ ├── serializers.py # Serializers +│ │ │ └── urls.py # /api/v1/writer/ routes +│ │ │ +│ │ ├── linker/ # Internal linking +│ │ │ ├── views.py # LinkerViewSet +│ │ │ └── urls.py # /api/v1/linker/ routes +│ │ │ +│ │ ├── optimizer/ # Content optimization +│ │ │ ├── views.py # OptimizerViewSet +│ │ │ └── urls.py # /api/v1/optimizer/ routes +│ │ │ +│ │ ├── publisher/ # Publishing & deployment +│ │ │ ├── views.py # PublisherViewSet, PublishingRecordViewSet +│ │ │ └── urls.py # /api/v1/publisher/ routes +│ │ │ +│ │ ├── automation/ # Automation rules & tasks +│ │ │ ├── views.py # AutomationRuleViewSet, ScheduledTaskViewSet +│ │ │ └── urls.py # /api/v1/automation/ routes +│ │ │ +│ │ ├── integration/ # External platform integrations +│ │ │ ├── views.py # IntegrationViewSet +│ │ │ └── urls.py # /api/v1/integration/ routes +│ │ │ +│ │ ├── system/ # Settings, prompts, integrations +│ │ │ ├── models.py # AIPrompt, IntegrationSettings, AuthorProfile +│ │ │ ├── views.py # AIPromptViewSet, AuthorProfileViewSet +│ │ │ ├── settings_views.py # SystemSettingsViewSet +│ │ │ └── urls.py # /api/v1/system/ routes +│ │ │ +│ │ └── billing/ # Credits & transactions +│ │ ├── views.py # CreditBalanceViewSet, CreditTransactionViewSet +│ │ ├── services.py # CreditService +│ │ └── urls.py # /api/v1/billing/ routes +│ │ +│ ├── business/ # Business logic & models +│ │ ├── planning/ # Planner models +│ │ │ └── models.py # Keywords, Clusters, ContentIdeas +│ │ ├── content/ # Writer models +│ │ │ └── models.py # Tasks, Content, Images +│ │ ├── linking/ # Linker services +│ │ ├── optimization/ # Optimizer services +│ │ ├── publishing/ # Publisher models +│ │ ├── automation/ # Automation models +│ │ ├── integration/ # Integration models +│ │ │ ├── models.py # SiteIntegration +│ │ │ └── services/ # IntegrationService, SyncService +│ │ └── billing/ # Billing models +│ │ +│ ├── ai/ # AI Framework +│ │ ├── engine.py # AIEngine - Central orchestrator +│ │ ├── ai_core.py # AICore - API calls & model selection +│ │ ├── base.py # BaseAIFunction - Function interface +│ │ ├── registry.py # AI function registry +│ │ ├── prompts.py # PromptRegistry +│ │ ├── tracker.py # Progress & cost tracking +│ │ ├── tasks.py # run_ai_task Celery wrapper +│ │ ├── settings.py # Model configuration +│ │ ├── validators.py # Validation functions +│ │ ├── constants.py # AI constants +│ │ └── functions/ # AI function implementations +│ │ ├── auto_cluster.py # Keyword clustering +│ │ ├── generate_ideas.py # Content idea generation +│ │ ├── generate_content.py # Blog post generation +│ │ ├── generate_image_prompts.py # Image prompt extraction +│ │ └── generate_images.py # Image generation (DALL-E/Runware) +│ │ +│ ├── api/ # API base classes +│ │ ├── base.py # AccountModelViewSet, SiteSectorModelViewSet +│ │ ├── response.py # success_response, error_response +│ │ ├── pagination.py # CustomPageNumberPagination +│ │ ├── permissions.py # Custom permissions +│ │ ├── throttles.py # Rate limiting +│ │ └── exception_handlers.py # Unified error handling +│ │ +│ ├── middleware/ # Custom middleware +│ │ ├── request_id.py # RequestIDMiddleware +│ │ ├── resource_tracker.py # ResourceTrackingMiddleware +│ │ └── __init__.py +│ │ +│ ├── utils/ # Shared utilities +│ │ ├── wordpress.py # WordPress helpers +│ │ ├── content_normalizer.py # Content processing +│ │ ├── queue_manager.py # Queue utilities +│ │ └── ai_processor.py # Legacy AI utilities +│ │ +│ └── admin/ # Django admin customization +│ ├── base.py # BaseAdmin +│ └── site.py # Admin site configuration +│ +├── manage.py # Django management CLI +├── requirements.txt # Python dependencies +└── Dockerfile # Docker configuration +``` + +### Core Base Classes + +#### AccountModelViewSet +**Location:** `igny8_core/api/base.py` + +Automatic account filtering for all resources. Used by modules that don't need site/sector filtering. + +```python +class AccountModelViewSet(ModelViewSet): + """Filters queryset by request.account automatically""" + + def get_queryset(self): + return super().get_queryset().filter(account=self.request.account) +``` + +**Used by:** System, Billing, Auth modules + +#### SiteSectorModelViewSet +**Location:** `igny8_core/api/base.py` + +Automatic filtering by account, site, and sector. Used by content-related modules. + +```python +class SiteSectorModelViewSet(ModelViewSet): + """Filters by account, site, and sector from query params""" + + def get_queryset(self): + queryset = super().get_queryset() + site_id = self.request.query_params.get('site_id') + sector_id = self.request.query_params.get('sector_id') + # ... filtering logic +``` + +**Used by:** Planner, Writer, Publisher, Automation, Integration modules + +### Middleware Stack + +1. **RequestIDMiddleware** - Assigns unique ID to each request +2. **AccountContextMiddleware** - Sets `request.account` from JWT token +3. **ResourceTrackingMiddleware** - Tracks API usage metrics + +--- + +## Frontend Deep Dive + +### Technology Stack + +- **Framework:** React 19.0.0 +- **Build Tool:** Vite 6.1.0 +- **Routing:** React Router v7.9.5 +- **State:** Zustand 5.0.8 +- **Styling:** Tailwind CSS 4.0.8 +- **Charts:** ApexCharts 4.1.0 +- **Icons:** Heroicons 2.2.0 + +### Project Structure + +``` +frontend/ +├── src/ +│ ├── main.tsx # Application entry point +│ ├── App.tsx # Main app with routing +│ ├── index.css # Global styles +│ │ +│ ├── api/ # API client layer +│ │ ├── client.ts # Axios instance with interceptors +│ │ ├── auth.ts # Auth API calls +│ │ ├── planner.ts # Planner API calls +│ │ ├── writer.ts # Writer API calls +│ │ └── [module].ts # Other module APIs +│ │ +│ ├── store/ # Zustand state management +│ │ ├── authStore.ts # Auth state (user, tokens) +│ │ ├── siteStore.ts # Site context +│ │ ├── moduleStore.ts # Module-specific state +│ │ └── globalStore.ts # Global app state +│ │ +│ ├── pages/ # Page components +│ │ ├── AuthPages/ +│ │ │ ├── SignIn.tsx +│ │ │ └── SignUp.tsx +│ │ ├── Dashboard/ +│ │ │ └── Home.tsx +│ │ ├── Planner/ +│ │ │ ├── Dashboard.tsx +│ │ │ ├── Keywords.tsx +│ │ │ ├── Clusters.tsx +│ │ │ └── Ideas.tsx +│ │ ├── Writer/ +│ │ │ ├── Dashboard.tsx +│ │ │ ├── Tasks.tsx +│ │ │ ├── Content.tsx +│ │ │ ├── ContentView.tsx +│ │ │ ├── Drafts.tsx +│ │ │ ├── Images.tsx +│ │ │ └── Published.tsx +│ │ ├── Linker/ +│ │ │ ├── Dashboard.tsx +│ │ │ └── ContentList.tsx +│ │ ├── Optimizer/ +│ │ │ ├── Dashboard.tsx +│ │ │ ├── ContentSelector.tsx +│ │ │ └── AnalysisPreview.tsx +│ │ ├── Thinker/ # System settings +│ │ │ ├── Dashboard.tsx +│ │ │ ├── Prompts.tsx +│ │ │ ├── AuthorProfiles.tsx +│ │ │ └── Strategies.tsx +│ │ ├── Billing/ +│ │ │ ├── Credits.tsx +│ │ │ ├── Transactions.tsx +│ │ │ └── Usage.tsx +│ │ ├── Automation/ +│ │ │ ├── Dashboard.tsx +│ │ │ ├── Rules.tsx +│ │ │ └── Tasks.tsx +│ │ └── Settings/ +│ │ ├── General.tsx +│ │ ├── Users.tsx +│ │ ├── Subscriptions.tsx +│ │ ├── System.tsx +│ │ └── AI.tsx +│ │ +│ ├── components/ # Reusable components +│ │ ├── common/ # Common UI components +│ │ ├── auth/ # Auth components +│ │ ├── forms/ # Form components +│ │ └── [module]/ # Module-specific components +│ │ +│ ├── layout/ # Layout components +│ │ ├── AppLayout.tsx # Main app layout +│ │ ├── Sidebar.tsx # Navigation sidebar +│ │ ├── Header.tsx # Top header +│ │ └── Footer.tsx # Footer +│ │ +│ ├── hooks/ # Custom React hooks +│ │ ├── useAuth.ts # Auth hook +│ │ ├── useApi.ts # API call hook +│ │ └── useSite.ts # Site context hook +│ │ +│ ├── services/ # Business logic +│ │ ├── authService.ts # Auth logic +│ │ └── apiService.ts # API utilities +│ │ +│ ├── config/ # Configuration +│ │ └── constants.ts # App constants +│ │ +│ └── types/ # TypeScript types +│ ├── api.ts # API types +│ └── models.ts # Data model types +│ +├── public/ # Static assets +├── package.json # Node dependencies +├── vite.config.ts # Vite configuration +├── tsconfig.json # TypeScript configuration +└── tailwind.config.js # Tailwind configuration +``` + +### State Management (Zustand) + +**Auth Store** (`store/authStore.ts`) +```typescript +interface AuthState { + user: User | null; + token: string | null; + refreshToken: string | null; + isAuthenticated: boolean; + login: (email: string, password: string) => Promise; + logout: () => void; + refreshAccessToken: () => Promise; +} +``` + +**Site Store** (`store/siteStore.ts`) +```typescript +interface SiteState { + currentSite: Site | null; + currentSector: Sector | null; + sites: Site[]; + setSite: (site: Site) => void; + setSector: (sector: Sector) => void; +} +``` + +--- + +## WordPress Plugin Deep Dive + +### Plugin Structure + +``` +igny8-wp-integration/ +├── igny8-bridge.php # Main plugin file & bootstrap +├── uninstall.php # Cleanup on uninstall +│ +├── includes/ # Core classes +│ ├── class-igny8-api.php # API client (authentication, requests) +│ ├── class-igny8-rest-api.php # WP REST endpoints (/wp-json/igny8/v1/) +│ ├── class-igny8-site.php # Site data collection +│ ├── class-igny8-webhooks.php # Webhook handlers (incoming from IGNY8) +│ ├── class-igny8-webhook-logs.php # Webhook logging +│ ├── class-igny8-link-queue.php # Link processing queue +│ └── functions.php # Helper functions +│ +├── admin/ # WordPress admin UI +│ ├── class-admin.php # Settings page controller +│ ├── settings.php # Settings page UI +│ ├── class-admin-columns.php # Custom post columns +│ ├── class-post-meta-boxes.php # Post editor meta boxes +│ └── assets/ +│ ├── css/admin.css # Admin styles +│ └── js/ +│ ├── admin.js # Admin scripts +│ └── post-editor.js # Post editor enhancements +│ +├── sync/ # Synchronization logic +│ ├── hooks.php # WordPress action/filter hooks +│ ├── post-sync.php # WP → IGNY8 post sync +│ ├── taxonomy-sync.php # WP → IGNY8 taxonomy sync +│ └── igny8-to-wp.php # IGNY8 → WP content sync +│ +├── data/ # Data collection & mapping +│ ├── site-collection.php # Full site data collection +│ ├── semantic-mapping.php # Semantic metadata extraction +│ ├── link-graph.php # Internal link graph builder +│ └── woocommerce.php # WooCommerce integration +│ +├── docs/ # Plugin documentation +│ ├── WORDPRESS-PLUGIN-INTEGRATION.md +│ ├── AUTHENTICATION-AUDIT.md +│ └── SYNC-DATA-FLOW-DIAGRAM.md +│ +└── tests/ # Test scripts + ├── test-api-authentication.php + ├── test-site-metadata.php + └── test-sync-structure.php +``` + +### Core Classes + +#### Igny8API +**File:** `includes/class-igny8-api.php` + +Handles all API communication with IGNY8 backend. + +**Key Methods:** +- `connect($api_key, $site_id)` - Establish connection using API key +- `get($endpoint)` - GET request +- `post($endpoint, $data)` - POST request +- `put($endpoint, $data)` - PUT request +- `delete($endpoint)` - DELETE request +- `parse_response($response)` - Normalize API responses + +**Authentication:** +- Primary: API key (stored in `igny8_api_key` option) +- Sends as `X-IGNY8-API-Key` header +- Automatic retry on 401 errors + +#### Igny8RestAPI +**File:** `includes/class-igny8-rest-api.php` + +Exposes WordPress data via REST API for IGNY8 to consume. + +**Endpoints:** +- `GET /wp-json/igny8/v1/site-metadata/` - Site structure and metadata +- `GET /wp-json/igny8/v1/post-by-task-id/{task_id}` - Find post by IGNY8 task ID +- `GET /wp-json/igny8/v1/post-by-content-id/{content_id}` - Find post by content ID +- `POST /wp-json/igny8/v1/posts/` - Create post from IGNY8 content +- `PUT /wp-json/igny8/v1/posts/{id}` - Update post from IGNY8 + +**Authentication:** +- Accepts `X-IGNY8-API-Key` header +- Validates against stored API key + +#### Igny8Site +**File:** `includes/class-igny8-site.php` + +Collects and formats site data for IGNY8. + +**Key Methods:** +- `get_site_metadata()` - Collect post types, taxonomies, counts +- `get_posts_data($post_type, $limit)` - Export posts +- `get_taxonomy_data($taxonomy, $limit)` - Export taxonomy terms +- `send_to_igny8()` - Push site data to IGNY8 API + +### Sync Flow + +#### WP → IGNY8 (Post Status Updates) + +1. User edits/publishes post in WordPress +2. `save_post` hook triggers → `sync/post-sync.php` +3. Check if post has `_igny8_task_id` meta +4. Map WP status to IGNY8 status +5. Send PUT request to `/api/v1/writer/tasks/{task_id}/` +6. Update `_igny8_last_synced` meta + +#### IGNY8 → WP (Content Publishing) + +1. IGNY8 creates content via AI Writer +2. IGNY8 sends POST to `/wp-json/igny8/v1/posts/` +3. Plugin validates API key +4. Create WP post with content +5. Store `_igny8_task_id` and `_igny8_content_id` meta +6. Return post ID and URL to IGNY8 + +### Post Meta Fields + +All IGNY8-managed posts use prefixed meta keys: + +- `_igny8_task_id` - IGNY8 task ID +- `_igny8_content_id` - IGNY8 content ID +- `_igny8_wordpress_status` - Current WP status +- `_igny8_last_synced` - Last sync timestamp +- `_igny8_managed` - Flag for IGNY8-managed posts + +--- + +## Integration Architecture + +### WordPress ↔ IGNY8 Integration + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ WordPress Site │ +├─────────────────────────────────────────────────────────────────┤ +│ IGNY8 Bridge Plugin │ +│ ───────────────────────────────────────────────────────────── │ +│ 1. REST API Endpoints (/wp-json/igny8/v1/) │ +│ • site-metadata (expose WP structure) │ +│ • posts (CRUD operations) │ +│ • post-by-task-id (lookup) │ +│ │ +│ 2. Sync Handlers (sync/) │ +│ • post-sync.php (WP → IGNY8) │ +│ • igny8-to-wp.php (IGNY8 → WP) │ +│ │ +│ 3. API Client (Igny8API class) │ +│ • Authentication (API key) │ +│ • Request/Response handling │ +└─────────────────────┬───────────────────────────────────────────┘ + │ + │ HTTPS REST API + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ IGNY8 Backend (Django) │ +├─────────────────────────────────────────────────────────────────┤ +│ Integration Module (/api/v1/integration/) │ +│ ───────────────────────────────────────────────────────────── │ +│ 1. IntegrationViewSet │ +│ • test-connection (validate WP connection) │ +│ • update-structure (receive WP metadata) │ +│ • sync (bidirectional sync trigger) │ +│ │ +│ 2. SiteIntegration Model │ +│ • platform: 'wordpress' │ +│ • credentials_json: {api_key, username, app_password} │ +│ • config_json: {site_url, content_types, sync_settings} │ +│ │ +│ 3. Integration Services │ +│ • IntegrationService (connection testing) │ +│ • SyncService (bidirectional sync) │ +│ • ContentSyncService (content mapping) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Authentication Flow + +**Plugin → Backend:** +1. Plugin stores API key in WP options (`igny8_api_key`) +2. All API requests include `X-IGNY8-API-Key` header +3. Backend validates key against `Site.wp_api_key` field +4. Backend sets account context from API key + +**Backend → Plugin:** +1. Backend sends requests to WP REST endpoints +2. Includes `X-IGNY8-API-Key` header +3. Plugin validates key matches stored value +4. Plugin processes request and returns unified response + +--- + +## AI Framework + +### Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ AI Framework Entry Point │ +├─────────────────────────────────────────────────────────────────┤ +│ run_ai_task(function_name, payload, account, celery_task) │ +│ • Called from ViewSets for async AI operations │ +│ • Wraps AI execution in Celery task │ +└─────────────────────┬───────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ AIEngine │ +│ Location: igny8_core/ai/engine.py │ +├─────────────────────────────────────────────────────────────────┤ +│ • Central orchestrator for all AI functions │ +│ • Progress tracking (Celery task updates) │ +│ • Cost tracking (token usage) │ +│ • Retry logic │ +│ • Error handling │ +│ │ +│ Key Methods: │ +│ • execute(function_name, payload) → result │ +│ • _execute_function(func, data) → processes function │ +│ • _update_progress(step, status, message) → UI updates │ +└─────────────────────┬───────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ AI Function Registry │ +│ Location: igny8_core/ai/registry.py │ +├─────────────────────────────────────────────────────────────────┤ +│ Registered Functions: │ +│ • auto_cluster (keywords → clusters) │ +│ • generate_ideas (clusters → content ideas) │ +│ • generate_content (ideas → blog posts) │ +│ • generate_image_prompts (content → image prompts) │ +│ • generate_images (prompts → images) │ +└─────────────────────┬───────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ AICore │ +│ Location: igny8_core/ai/ai_core.py │ +├─────────────────────────────────────────────────────────────────┤ +│ • Handles API calls to OpenAI/Runware │ +│ • Model selection and configuration │ +│ • Response parsing (JSON/text) │ +│ • Console logging │ +│ • Token counting and cost calculation │ +│ │ +│ Key Methods: │ +│ • run_ai_request(prompt, model, ...) → AI response │ +│ • generate_image(prompt, model, ...) → image URL │ +│ • _call_openai_api(messages, model, ...) → raw response │ +│ • _parse_json_response(text) → parsed JSON │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### AI Functions + +#### auto_cluster +**File:** `ai/functions/auto_cluster.py` +**Input:** List of keyword IDs +**Output:** Clusters with assigned keywords +**Model:** GPT-4 or configured model +**Process:** +1. Load keywords from database +2. Build prompt with keyword list +3. Call AI to group semantically similar keywords +4. Parse JSON response +5. Create Cluster records +6. Assign keywords to clusters + +#### generate_ideas +**File:** `ai/functions/generate_ideas.py` +**Input:** List of cluster IDs +**Output:** Content ideas for each cluster +**Model:** GPT-4 or configured model +**Process:** +1. Load clusters and keywords +2. Build prompt for content ideation +3. Call AI to generate ideas +4. Parse response +5. Create ContentIdea records + +#### generate_content +**File:** `ai/functions/generate_content.py` +**Input:** List of task IDs +**Output:** Full blog posts with HTML +**Model:** GPT-4 or configured model +**Process:** +1. Load tasks and content ideas +2. Build detailed prompt with brief +3. Call AI to generate article +4. Parse HTML content +5. Create/update Content records +6. Update task status + +#### generate_images +**File:** `ai/functions/generate_images.py` +**Input:** List of task IDs with image prompts +**Output:** Generated image URLs +**Models:** DALL-E 3 or Runware +**Process:** +1. Load tasks with image prompts +2. For each prompt, call image API +3. Upload images to storage +4. Create Image records +5. Link images to content + +--- + +## Data Models + +### Account & Auth Models + +**Account** (`auth/models.py`) +```python +Account +├── name: str +├── slug: str (unique) +├── owner: User (FK) +├── plan: Plan (FK) +├── credits: int +├── status: str (active, suspended, trial, cancelled) +└── created_at, updated_at +``` + +**User** (`auth/models.py`) +```python +User (extends AbstractUser) +├── email: str (unique) +├── username: str (unique) +├── account: Account (FK) +├── role: str (developer, owner, admin, editor, viewer, system_bot) +└── is_active, is_staff, is_superuser +``` + +**Site** (`auth/models.py`) +```python +Site +├── account: Account (FK) +├── name: str +├── slug: str +├── domain: URL +├── industry: Industry (FK, nullable) +├── wordpress_url: URL +├── wordpress_username: str +├── wordpress_password: str (encrypted) +├── wp_api_key: str (for bridge auth) +├── status: str (active, inactive, suspended) +└── created_at, updated_at +``` + +**Sector** (`auth/models.py`) +```python +Sector +├── account: Account (FK) +├── site: Site (FK) +├── name: str +├── industry_sector: IndustrySector (FK, nullable) +├── status: str (active, inactive) +└── created_at, updated_at +``` + +### Planner Models + +**Keywords** (`business/planning/models.py`) +```python +Keywords (inherits SiteSectorBaseModel) +├── account, site, sector (from base) +├── keyword: str +├── search_volume: int +├── difficulty: int +├── intent: str (informational, commercial, transactional, navigational) +├── status: str (active, inactive, used) +├── source: str (manual, csv_import, api_import) +└── created_at, updated_at +``` + +**Clusters** (`business/planning/models.py`) +```python +Clusters (inherits SiteSectorBaseModel) +├── account, site, sector (from base) +├── name: str +├── description: text +├── keywords: ManyToMany(Keywords) +├── status: str (active, archived) +└── created_at, updated_at + +# Stage 1 Changes (Nov 2025): +# REMOVED: context_type, dimension_meta +# Now pure topic-based clustering +``` + +**ContentIdeas** (`business/planning/models.py`) +```python +ContentIdeas (inherits SiteSectorBaseModel) +├── account, site, sector (from base) +├── cluster: Cluster (FK) +├── title: str +├── description: text +├── suggested_keywords: JSON +├── status: str (draft, approved, in_progress, completed) +└── created_at, updated_at +``` + +### Writer Models + +**Tasks** (`business/content/models.py`) +```python +Tasks (inherits SiteSectorBaseModel) +├── account, site, sector (from base) +├── cluster: Cluster (FK, nullable) +├── title: str +├── description: text (brief) +├── keywords: JSON (target keywords) +├── content_type: str (post, page, product, etc.) +├── content_structure: JSON (template for content generation) +├── taxonomy_term_id: int (optional categorization) +├── status: str (queued, completed) # Simplified in Stage 1 +├── assigned_post_id: int (WP post ID) +├── post_url: URL +└── created_at, updated_at + +# Stage 1 Changes (Nov 2025): +# REMOVED: content_idea FK, cluster_role, entity_type, cluster_context, dimension_roles, metadata, content_record +# ADDED: content_type, content_structure, taxonomy_term_id +# CHANGED: status simplified to (queued, completed) +``` + +**Content** (`business/content/models.py`) +```python +Content (inherits SiteSectorBaseModel) +├── account, site, sector (from base) +├── cluster_id: int (cluster reference) +├── title: str +├── content_html: text (generated HTML content) +├── content_type: str (post, page, product, etc.) +├── content_structure: JSON (structure template used) +├── taxonomy_terms: ManyToMany(ContentTaxonomy) # Direct M2M +├── external_id: str (WP post ID) +├── external_url: URL (published URL) +├── source: str (igny8, wordpress, import) +├── status: str (draft, published) # Simplified in Stage 1 +└── created_at, updated_at + +# Stage 1 Changes (Nov 2025): +# REMOVED: task FK, html_content, word_count, metadata, meta_title, meta_description, +# primary_keyword, secondary_keywords, entity_type, json_blocks, structure_data, +# content_format, cluster_role, sync_status, external_type, external_status, +# sync_data, last_synced_at, validation_errors, is_validated +# ADDED: cluster_id, title, content_html, content_type, content_structure +# CHANGED: status simplified to (draft, published) +# CHANGED: taxonomy relationship from through model to direct M2M +``` + +**Images** (`business/content/models.py`) +```python +Images (inherits SiteSectorBaseModel) +├── account, site, sector (from base) +├── task: Tasks (FK) +├── image_url: URL +├── prompt: text +├── is_featured: bool +├── position: int +├── alt_text: str +├── caption: str +└── created_at, updated_at +``` + +**ContentTaxonomy** (`business/content/models.py`) +```python +ContentTaxonomy (inherits SiteSectorBaseModel) +├── account, site, sector (from base) +├── name: str +├── slug: str +├── taxonomy_type: str (category, post_tag, cluster) +├── external_id: str (WordPress taxonomy term ID) +├── external_taxonomy: str (WordPress taxonomy name) +└── created_at, updated_at + +# Stage 1 Changes (Nov 2025): +# REMOVED: description, parent FK, sync_status, count, metadata, clusters M2M +# ADDED: 'cluster' as taxonomy_type option +# Simplified to essential fields for WP sync +``` + +### Integration Models + +**SiteIntegration** (`business/integration/models.py`) +```python +SiteIntegration +├── account: Account (FK) +├── site: Site (FK) +├── platform: str (wordpress, shopify, wix) +├── platform_type: str (cms, ecommerce, builder) +├── config_json: JSON (site_url, content_types, sync_settings) +├── credentials_json: JSON (api_key, username, app_password) +├── is_active: bool +├── sync_enabled: bool +├── last_sync_at: datetime +└── created_at, updated_at +``` + +--- + +## API Reference + +### Base URL + +``` +Production: https://api.igny8.com/api/v1/ +Development: http://localhost:8000/api/v1/ +``` + +### Unified Response Format + +All endpoints return: + +```json +{ + "success": true, + "data": { ... }, + "message": "Success message", + "request_id": "uuid", + "count": 100, // For paginated endpoints + "next": "url", // Pagination + "previous": "url", // Pagination + "results": [...] // Paginated data +} +``` + +Error response: +```json +{ + "success": false, + "error": "Error message", + "errors": {...}, // Field-level errors + "request_id": "uuid" +} +``` + +### Authentication + +**Login:** +```http +POST /api/v1/auth/login/ +Content-Type: application/json + +{ + "email": "user@example.com", + "password": "password" +} + +Response: +{ + "success": true, + "data": { + "access": "jwt_token", + "refresh": "refresh_token", + "user": {...} + } +} +``` + +**Use Token:** +```http +GET /api/v1/planner/keywords/ +Authorization: Bearer {access_token} +``` + +### Key Endpoints by Module + +#### Planner Module + +``` +GET /api/v1/planner/keywords/ # List keywords +POST /api/v1/planner/keywords/ # Create keyword +GET /api/v1/planner/keywords/{id}/ # Get keyword +PUT /api/v1/planner/keywords/{id}/ # Update keyword +DELETE /api/v1/planner/keywords/{id}/ # Delete keyword +POST /api/v1/planner/keywords/bulk_create/ # Bulk import +POST /api/v1/planner/keywords/auto_cluster/ # AI clustering + +GET /api/v1/planner/clusters/ # List clusters +POST /api/v1/planner/clusters/ # Create cluster +POST /api/v1/planner/clusters/generate_ideas/ # AI idea generation + +GET /api/v1/planner/ideas/ # List ideas +POST /api/v1/planner/ideas/ # Create idea +``` + +#### Writer Module + +``` +GET /api/v1/writer/tasks/ # List tasks +POST /api/v1/writer/tasks/ # Create task +GET /api/v1/writer/tasks/{id}/ # Get task +PUT /api/v1/writer/tasks/{id}/ # Update task +POST /api/v1/writer/tasks/generate_content/ # AI content generation +POST /api/v1/writer/tasks/generate_images/ # AI image generation + +GET /api/v1/writer/content/ # List content +GET /api/v1/writer/content/{id}/ # Get content + +GET /api/v1/writer/images/ # List images +``` + +#### Integration Module + +``` +GET /api/v1/integration/integrations/ # List integrations +POST /api/v1/integration/integrations/ # Create integration +POST /api/v1/integration/integrations/test-connection/ # Test WP connection +POST /api/v1/integration/integrations/{id}/update-structure/ # Update WP metadata +GET /api/v1/integration/integrations/{id}/content-types/ # Get content types +POST /api/v1/integration/integrations/{id}/sync/ # Trigger sync +``` + +#### System Module + +``` +GET /api/v1/system/prompts/ # List AI prompts +POST /api/v1/system/prompts/ # Create prompt +GET /api/v1/system/author-profiles/ # List author profiles +GET /api/v1/system/settings/ # System settings +``` + +#### Billing Module + +``` +GET /api/v1/billing/balance/ # Get credit balance +GET /api/v1/billing/transactions/ # List transactions +GET /api/v1/billing/usage/ # Usage analytics +``` + +--- + +## Workflows + +### 1. Keyword to Published Post (Full Workflow) + +``` +1. Import Keywords + ↓ + POST /api/v1/planner/keywords/bulk_create/ + • Upload CSV or manual entry + • Keywords stored in database + +2. AI Clustering + ↓ + POST /api/v1/planner/keywords/auto_cluster/ + • Celery task: auto_cluster + • AI groups keywords → Clusters + • Keywords assigned to clusters + +3. Generate Content Ideas + ↓ + POST /api/v1/planner/clusters/generate_ideas/ + • Celery task: generate_ideas + • AI creates ContentIdeas from clusters + +4. Create Writer Task + ↓ + POST /api/v1/writer/tasks/ + • Link task to ContentIdea + • Set brief and target keywords + +5. Generate Content + ↓ + POST /api/v1/writer/tasks/generate_content/ + • Celery task: generate_content + • AI writes full blog post → Content record + • Task status: completed + +6. Generate Images (optional) + ↓ + POST /api/v1/writer/tasks/generate_images/ + • Celery task: generate_images + • AI creates images → Image records + +7. Publish to WordPress + ↓ + POST /wp-json/igny8/v1/posts/ + • IGNY8 sends content to WP plugin + • Plugin creates WP post + • Stores _igny8_task_id meta + +8. Status Sync (WP → IGNY8) + ↓ + PUT /api/v1/writer/tasks/{id}/ + • User publishes in WP + • Plugin syncs status back to IGNY8 +``` + +### 2. WordPress Integration Setup + +``` +1. Install Plugin + ↓ + • Upload igny8-bridge.zip to WordPress + • Activate plugin + +2. Generate API Key (IGNY8 App) + ↓ + • Login to app.igny8.com + • Navigate to Settings → Sites + • Generate API key for site + +3. Connect Plugin + ↓ + • In WP admin: Settings → IGNY8 API + • Enter email, password, API key + • Click "Test Connection" + +4. Initial Sync + ↓ + • Plugin sends site metadata to IGNY8 + • POST /api/v1/integration/integrations/test-connection/ + • IGNY8 creates SiteIntegration record + +5. Ongoing Sync + ↓ + • WP → IGNY8: Post status updates (save_post hook) + • IGNY8 → WP: Content publishing (REST API) +``` + +--- + +## Developer Navigation Map + +### "I need to..." + +#### Add a new API endpoint +1. Create method in ViewSet (`modules/[module]/views.py`) +2. Use `@action` decorator for custom endpoints +3. Add route to `urls.py` if needed +4. Return `success_response()` or `error_response()` + +#### Add a new model field +1. Edit model in `business/[domain]/models.py` +2. Run `python manage.py makemigrations` +3. Run `python manage.py migrate` +4. Update serializers in `modules/[module]/serializers.py` + +#### Add AI functionality +1. Create function class in `ai/functions/[name].py` +2. Inherit from `BaseAIFunction` +3. Implement `validate()`, `prepare()`, `execute()`, `save()` +4. Register in `ai/registry.py` +5. Call via `run_ai_task()` from ViewSet + +#### Debug API call +1. Check request ID in response +2. Search logs: `grep {request_id} logs/` +3. Check middleware: `RequestIDMiddleware` +4. Check account context: `AccountContextMiddleware` + +#### Add WordPress sync logic +1. Add hook in `sync/hooks.php` +2. Implement handler in `sync/post-sync.php` or `sync/taxonomy-sync.php` +3. Use `Igny8API` class for API calls +4. Update post meta with `_igny8_*` keys + +#### Add frontend page +1. Create component in `frontend/src/pages/[Module]/[Page].tsx` +2. Add route in `App.tsx` +3. Add to sidebar navigation in `layout/Sidebar.tsx` +4. Create API calls in `api/[module].ts` +5. Use Zustand store for state + +--- + +## Troubleshooting Map + +### Common Issues and Where to Look + +#### "Authentication failed / 401 errors" +**Check:** +- `igny8_core/auth/middleware.py` - AccountContextMiddleware +- JWT token expiry - tokens expire in 15 minutes +- API key validation - `Site.wp_api_key` field +- Plugin: `includes/class-igny8-api.php` - connect() method + +#### "Posts not syncing to IGNY8" +**Check:** +- `sync/hooks.php` - save_post hook registered? +- `sync/post-sync.php` - igny8_sync_post_status_to_igny8() +- Post meta: `_igny8_task_id` exists? +- `igny8_is_connection_enabled()` returns true? +- API key stored: `get_option('igny8_api_key')` + +#### "AI task stuck/failed" +**Check:** +- Celery worker running: `celery -A igny8_core worker -l info` +- Task logs: Check Celery console output +- `ai/engine.py` - AIEngine.execute() +- `ai/ai_core.py` - APICore.run_ai_request() +- Credit balance: Check `Account.credits` +- Model configuration: `system/models.py` - IntegrationSettings + +#### "Frontend can't fetch data" +**Check:** +- CORS settings: `igny8_core/settings.py` - CORS_ALLOWED_ORIGINS +- API base URL: `frontend/src/api/client.ts` +- Auth token: Check `authStore` in browser DevTools +- Network tab: Check request/response +- Backend logs: Check for errors + +#### "WordPress plugin can't connect" +**Check:** +- API key matches: WP option vs `Site.wp_api_key` +- Site URL correct: WP site URL vs `SiteIntegration.config_json.site_url` +- REST API enabled: Test `/wp-json/` endpoint +- PHP errors: Check WP debug log +- Connection test: `includes/class-igny8-api.php` - connect() + +#### "Rate limiting errors" +**Check:** +- `api/throttles.py` - DebugScopedRateThrottle +- `settings.py` - IGNY8_DEBUG_THROTTLE flag +- User role has sufficient permissions +- Check throttle scope in ViewSet + +#### "Account isolation not working" +**Check:** +- `auth/middleware.py` - AccountContextMiddleware sets request.account? +- ViewSet inherits from `AccountModelViewSet` or `SiteSectorModelViewSet`? +- JWT token contains `account_id`? +- Query filtering: `get_queryset()` filters by account? + +--- + +## File Quick Reference + +### Most Important Files + +| File | Purpose | When to Check | +|------|---------|---------------| +| `backend/igny8_core/settings.py` | Django configuration | Environment issues, installed apps | +| `backend/igny8_core/urls.py` | URL routing | Adding endpoints, 404 errors | +| `backend/igny8_core/celery.py` | Celery config | Task queue issues | +| `backend/igny8_core/auth/models.py` | Account, User, Site models | Multi-tenancy, auth issues | +| `backend/igny8_core/auth/middleware.py` | Account context | Request.account issues | +| `backend/igny8_core/api/base.py` | Base ViewSets | Queryset filtering | +| `backend/igny8_core/ai/engine.py` | AI orchestrator | AI task failures | +| `backend/igny8_core/ai/ai_core.py` | AI API calls | OpenAI errors | +| `frontend/src/App.tsx` | Frontend routing | Navigation issues | +| `frontend/src/store/authStore.ts` | Auth state | Login/logout issues | +| `frontend/src/api/client.ts` | API client | API call failures | +| `igny8-wp-integration/igny8-bridge.php` | Plugin bootstrap | Plugin initialization | +| `igny8-wp-integration/includes/class-igny8-api.php` | WP API client | API communication | +| `igny8-wp-integration/includes/class-igny8-rest-api.php` | WP REST endpoints | Endpoint errors | +| `igny8-wp-integration/sync/post-sync.php` | WP → IGNY8 sync | Sync failures | + +--- + +## Technology Dependencies + +### Backend + +``` +Django==5.2.7 +djangorestframework +psycopg2-binary # PostgreSQL +redis # Celery broker +celery==5.3.0 +PyJWT==2.8.0 +requests==2.31.0 +beautifulsoup4==4.12.0 +drf-spectacular==0.27.0 +django-filter +django-cors-headers +whitenoise +gunicorn +``` + +### Frontend + +``` +react==19.0.0 +react-dom==19.0.0 +react-router==7.1.5 +react-router-dom==7.9.5 +zustand==5.0.8 +vite==6.1.0 +typescript==5.7.2 +tailwindcss==4.0.8 +apexcharts==4.1.0 +@heroicons/react==2.2.0 +``` + +### WordPress Plugin + +``` +PHP >= 7.4 +WordPress >= 5.0 +``` + +--- + +## Portable Package Structure + +This section defines the **essential files and folders** needed to deploy IGNY8 to a new infrastructure with the same tech stack. These are IGNY8-specific application files (excluding standard framework boilerplate that gets installed via package managers). + +### Backend Essential Package Tree + +**Package Only These Files** - Excludes Django-generated boilerplate and dependencies installed via pip: + +``` +backend/ +├── manage.py # Django entry point (IGNY8-configured) +├── requirements.txt # Python dependencies (essential) +├── Dockerfile # Container configuration (essential) +│ +└── igny8_core/ # Main Django project + ├── __init__.py # Project marker + ├── settings.py # ⭐ IGNY8 configuration (critical) + ├── test_settings.py # Test configuration + ├── urls.py # Main URL routing (IGNY8-specific) + ├── asgi.py # ASGI entry point + ├── wsgi.py # WSGI entry point + ├── celery.py # ⭐ Celery configuration (critical) + │ + ├── admin/ # Custom Django admin + │ ├── __init__.py + │ ├── apps.py + │ ├── base.py # Admin base classes + │ └── site.py # Custom admin site + │ + ├── auth/ # ⭐ Authentication module (critical) + │ ├── __init__.py + │ ├── apps.py + │ ├── admin.py + │ ├── models.py # User, Account, Site models + │ ├── serializers.py + │ ├── views.py + │ ├── urls.py + │ ├── permissions.py + │ ├── middleware.py # Account context middleware + │ ├── utils.py + │ └── management/ + │ └── commands/ # Custom management commands + │ + ├── ai/ # ⭐ AI Framework (critical) + │ ├── __init__.py + │ ├── apps.py + │ ├── admin.py + │ ├── models.py # AIJob, AITaskResult + │ ├── base.py # Base AI classes + │ ├── ai_core.py # Core AI logic + │ ├── engine.py # AI execution engine + │ ├── registry.py # AI function registry + │ ├── prompts.py # AI prompt templates + │ ├── validators.py + │ ├── tracker.py # Progress tracking + │ ├── tasks.py # Celery tasks + │ ├── settings.py # AI configuration + │ ├── constants.py + │ ├── functions/ # AI function implementations + │ │ ├── __init__.py + │ │ ├── base.py + │ │ ├── content_generation.py + │ │ ├── image_generation.py + │ │ └── ... + │ ├── migrations/ # Database migrations (critical) + │ └── tests/ + │ + ├── api/ # ⭐ API Infrastructure (critical) + │ ├── __init__.py + │ ├── base.py # Base ViewSet classes + │ ├── response.py # Unified response format + │ ├── pagination.py # Pagination classes + │ ├── permissions.py # API permissions + │ ├── authentication.py # JWT authentication + │ ├── throttles.py # Rate limiting + │ ├── exception_handlers.py # Error handling + │ ├── schema_extensions.py # OpenAPI schema + │ ├── wordpress_publishing.py # WordPress integration + │ └── tests/ + │ + ├── business/ # ⭐ Business Logic Layer (critical) + │ ├── __init__.py + │ ├── planning/ + │ │ ├── __init__.py + │ │ ├── cluster_service.py + │ │ ├── keyword_service.py + │ │ └── idea_service.py + │ ├── content/ + │ │ ├── __init__.py + │ │ ├── generation_service.py + │ │ └── image_service.py + │ ├── linking/ + │ │ ├── __init__.py + │ │ └── linking_service.py + │ ├── optimization/ + │ │ ├── __init__.py + │ │ └── optimization_service.py + │ ├── publishing/ + │ │ ├── __init__.py + │ │ └── wordpress_service.py + │ ├── site_building/ + │ │ ├── __init__.py + │ │ └── builder_service.py + │ ├── automation/ + │ │ ├── __init__.py + │ │ └── automation_service.py + │ ├── integration/ + │ │ ├── __init__.py + │ │ └── integration_service.py + │ └── billing/ + │ ├── __init__.py + │ └── credit_service.py + │ + ├── modules/ # ⭐ Feature Modules (critical) + │ ├── __init__.py + │ │ + │ ├── planner/ # Keyword planning + │ │ ├── __init__.py + │ │ ├── apps.py + │ │ ├── admin.py + │ │ ├── models.py + │ │ ├── serializers.py + │ │ ├── cluster_serializers.py + │ │ ├── views.py + │ │ ├── urls.py + │ │ ├── migrations/ + │ │ └── management/ + │ │ + │ ├── writer/ # Content generation + │ │ ├── __init__.py + │ │ ├── apps.py + │ │ ├── admin.py + │ │ ├── models.py + │ │ ├── serializers.py + │ │ ├── views.py + │ │ ├── urls.py + │ │ ├── migrations/ + │ │ └── management/ + │ │ + │ ├── linker/ # Internal linking + │ │ ├── __init__.py + │ │ ├── apps.py + │ │ ├── admin.py + │ │ ├── models.py + │ │ ├── serializers.py + │ │ ├── views.py + │ │ ├── urls.py + │ │ └── migrations/ + │ │ + │ ├── optimizer/ # Content optimization + │ │ ├── __init__.py + │ │ ├── apps.py + │ │ ├── admin.py + │ │ ├── models.py + │ │ ├── serializers.py + │ │ ├── views.py + │ │ ├── urls.py + │ │ └── migrations/ + │ │ + │ ├── publisher/ # WordPress publishing + │ │ ├── __init__.py + │ │ ├── apps.py + │ │ ├── views.py + │ │ └── urls.py + │ │ + │ ├── site_builder/ # Site blueprints + │ │ ├── __init__.py + │ │ ├── apps.py + │ │ ├── admin.py + │ │ ├── models.py + │ │ ├── serializers.py + │ │ ├── views.py + │ │ ├── urls.py + │ │ └── migrations/ + │ │ + │ ├── automation/ # Task automation + │ │ ├── __init__.py + │ │ ├── apps.py + │ │ ├── admin.py + │ │ ├── models.py + │ │ ├── serializers.py + │ │ ├── views.py + │ │ ├── urls.py + │ │ └── migrations/ + │ │ + │ ├── integration/ # WordPress integration + │ │ ├── __init__.py + │ │ ├── apps.py + │ │ ├── views.py + │ │ └── urls.py + │ │ + │ ├── billing/ # Credit system + │ │ ├── __init__.py + │ │ ├── apps.py + │ │ ├── admin.py + │ │ ├── models.py + │ │ ├── serializers.py + │ │ ├── views.py + │ │ ├── urls.py + │ │ └── migrations/ + │ │ + │ └── system/ # System settings + │ ├── __init__.py + │ ├── apps.py + │ ├── admin.py + │ ├── models.py # Settings, AI models + │ ├── serializers.py + │ ├── settings_models.py + │ ├── settings_serializers.py + │ ├── settings_views.py + │ ├── settings_admin.py + │ ├── views.py + │ ├── integration_views.py + │ ├── urls.py + │ ├── validators.py + │ ├── utils.py + │ ├── schemas.py + │ ├── management/ + │ └── migrations/ + │ + ├── middleware/ # Custom middleware + │ ├── __init__.py + │ ├── request_id.py # Request ID tracking + │ └── resource_tracker.py # Resource monitoring + │ + ├── tasks/ # Celery tasks + │ └── wordpress_publishing.py + │ + ├── utils/ # Shared utilities + │ ├── __init__.py + │ ├── ai_processor.py + │ ├── content_normalizer.py + │ ├── queue_manager.py + │ └── wordpress.py + │ + ├── urls/ # URL modules + │ └── [module-specific URL configs] + │ + └── migrations/ # ⭐ Database migrations (critical) + └── [all migration files] +``` + +**EXCLUDE from package (installed via package managers):** +- `.venv/` - Virtual environment (created on deployment) +- `__pycache__/` - Python cache files +- `*.pyc`, `*.pyo` - Compiled Python +- `staticfiles/` - Collected static files (generated) +- `logs/` - Log files +- `db.sqlite3`, `*.sql` - Database files +- `celerybeat-schedule` - Celery scheduler state +- Test/debug scripts: `check_*.py`, `fix_*.py`, `verify_*.py`, `diagnose_*.py`, `inject_*.py`, `test_*.py` +- `backup_*.sql` - Database backups + +### Frontend Essential Package Tree + +**Package Only These Files** - Excludes React boilerplate and node_modules: + +``` +frontend/ +├── package.json # ⭐ Dependencies manifest (critical) +├── package-lock.json # Locked dependencies +├── tsconfig.json # TypeScript config (IGNY8-modified) +├── tsconfig.app.json # App-specific TS config +├── tsconfig.node.json # Node-specific TS config +├── vite.config.ts # ⭐ Vite build config (IGNY8-customized) +├── vitest.config.ts # Test configuration +├── postcss.config.js # PostCSS config +├── eslint.config.js # ESLint configuration +├── index.html # Main HTML entry point +├── marketing.html # Marketing site entry (if used) +├── Dockerfile # ⭐ Container config (critical) +├── Dockerfile.dev # Dev container config +├── Caddyfile # Reverse proxy config +│ +├── public/ # Static assets +│ ├── images/ # IGNY8 images +│ └── marketing/ # Marketing assets +│ +└── src/ # ⭐ Application source (all critical) + ├── main.tsx # ⭐ App entry point (IGNY8-specific) + ├── App.tsx # ⭐ Main app component (IGNY8 routing) + ├── index.css # Global styles + ├── vite-env.d.ts # Vite type definitions + ├── svg.d.ts # SVG type definitions + │ + ├── config/ # ⭐ App configuration + │ ├── api.config.ts # API endpoints + │ ├── routes.config.ts # Route definitions + │ └── constants.ts # App constants + │ + ├── types/ # ⭐ TypeScript types (IGNY8 models) + │ ├── index.ts + │ ├── auth.types.ts + │ ├── planner.types.ts + │ ├── writer.types.ts + │ ├── linker.types.ts + │ ├── optimizer.types.ts + │ ├── site.types.ts + │ ├── billing.types.ts + │ └── ... + │ + ├── api/ # ⭐ API client layer + │ ├── automation.api.ts + │ ├── linker.api.ts + │ └── optimizer.api.ts + │ + ├── services/ # ⭐ Service layer + │ ├── api.ts # Base API service + │ ├── integration.api.ts + │ └── siteBuilder.api.ts + │ + ├── store/ # ⭐ Zustand state management + │ ├── authStore.ts + │ ├── billingStore.ts + │ ├── plannerStore.ts + │ ├── siteStore.ts + │ ├── siteDefinitionStore.ts + │ ├── settingsStore.ts + │ ├── sectorStore.ts + │ ├── onboardingStore.ts + │ ├── columnVisibilityStore.ts + │ └── pageSizeStore.ts + │ + ├── context/ # React Context providers + │ ├── ThemeContext.tsx + │ └── HeaderMetricsContext.tsx + │ + ├── hooks/ # Custom React hooks + │ ├── useAuth.ts + │ ├── useDebounce.ts + │ ├── useLocalStorage.ts + │ └── ... + │ + ├── layout/ # ⭐ App layout components + │ ├── AppLayout.tsx # Main layout + │ ├── AppHeader.tsx # Header + │ ├── AppSidebar.tsx # Sidebar navigation + │ ├── SidebarWidget.tsx + │ └── Backdrop.tsx + │ + ├── components/ # ⭐ IGNY8-specific components + │ ├── auth/ # Authentication + │ │ ├── ProtectedRoute.tsx + │ │ └── ... + │ │ + │ ├── common/ # Shared components + │ │ ├── ErrorBoundary.tsx + │ │ ├── LoadingStateMonitor.tsx + │ │ ├── GlobalErrorDisplay.tsx + │ │ ├── ScrollToTop.tsx + │ │ ├── ModuleGuard.tsx + │ │ └── ... + │ │ + │ ├── header/ # Header components + │ ├── sidebar/ # Sidebar components + │ ├── navigation/ # Navigation + │ ├── dashboard/ # Dashboard widgets + │ ├── content/ # Content components + │ ├── integration/ # Integration UI + │ ├── linker/ # Linker components + │ ├── optimizer/ # Optimizer components + │ ├── publishing/ # Publishing UI + │ ├── sites/ # Site management + │ ├── tasks/ # Task management + │ ├── onboarding/ # Onboarding flow + │ ├── debug/ # Debug tools + │ │ + │ ├── ui/ # UI primitives + │ │ ├── toast/ + │ │ ├── button/ + │ │ ├── modal/ + │ │ └── ... + │ │ + │ ├── tables/ # Table components + │ ├── form/ # Form components + │ ├── charts/ # Chart components + │ └── shared/ # Shared UI + │ + ├── pages/ # ⭐ Page components (routes) + │ ├── Dashboard/ + │ │ └── Home.tsx # Main dashboard + │ │ + │ ├── AuthPages/ # Auth pages + │ │ ├── SignIn.tsx + │ │ └── SignUp.tsx + │ │ + │ ├── Planner/ # Planner module + │ │ ├── Dashboard.tsx + │ │ ├── Keywords.tsx + │ │ ├── Clusters.tsx + │ │ ├── ClusterDetail.tsx + │ │ ├── Ideas.tsx + │ │ └── KeywordOpportunities.tsx + │ │ + │ ├── Writer/ # Writer module + │ │ ├── Dashboard.tsx + │ │ ├── Tasks.tsx + │ │ ├── ContentLibrary.tsx + │ │ └── ... + │ │ + │ ├── Linker/ # Linker module + │ ├── Optimizer/ # Optimizer module + │ ├── Sites/ # Site management + │ ├── Settings/ # Settings + │ ├── Billing/ # Billing + │ ├── Automation/ # Automation + │ ├── Help/ # Help pages + │ ├── Setup/ # Setup wizard + │ ├── Reference/ # Reference pages + │ ├── Thinker/ # Thinker module + │ └── OtherPage/ + │ └── NotFound.tsx + │ + ├── modules/ # Module-specific logic + │ └── siteBuilder/ + │ + ├── templates/ # Page templates + ├── icons/ # Custom icons + ├── styles/ # ⭐ IGNY8 styles + │ └── igny8-colors.css # Custom color scheme + │ + ├── utils/ # Utility functions + │ ├── dateUtils.ts + │ ├── formatters.ts + │ └── ... + │ + ├── marketing/ # Marketing site (optional) + │ └── [marketing components] + │ + └── __tests__/ # Tests + └── [test files] +``` + +**EXCLUDE from package (installed via npm):** +- `node_modules/` - Dependencies (installed via `npm install`) +- `dist/` - Build output (generated) +- `banner.png` - May be optional +- `DESIGN_SYSTEM.md`, `MIGRATION_GUIDE.md`, `README.md`, `LICENSE.md` - Documentation (optional) + +### Installation & Portability Guide + +#### Backend Deployment Steps + +```bash +# 1. Copy backend essential files to new infrastructure +# 2. Set up Python environment +python -m venv .venv +source .venv/bin/activate # or .venv\Scripts\activate on Windows + +# 3. Install dependencies from requirements.txt +pip install -r requirements.txt + +# 4. Configure environment variables (create .env file) +SECRET_KEY= +DEBUG=False +DATABASE_URL=postgresql://user:pass@host:5432/dbname +REDIS_URL=redis://redis:6379/0 +ALLOWED_HOSTS=yourdomain.com + +# 5. Run migrations to set up database +python manage.py migrate + +# 6. Create superuser +python manage.py createsuperuser + +# 7. Collect static files +python manage.py collectstatic --noinput + +# 8. Start services +# Application server +gunicorn igny8_core.wsgi:application --bind 0.0.0.0:8000 + +# Celery worker (separate terminal) +celery -A igny8_core worker --loglevel=info + +# Celery beat scheduler (separate terminal) +celery -A igny8_core beat --loglevel=info +``` + +#### Frontend Deployment Steps + +```bash +# 1. Copy frontend essential files to new infrastructure +# 2. Install dependencies from package.json +npm install +# or +npm ci # for production (uses package-lock.json) + +# 3. Configure environment (create .env file) +VITE_API_BASE_URL=https://api.yourdomain.com +VITE_APP_NAME=IGNY8 + +# 4. Build for production +npm run build + +# 5. Serve static files (multiple options) +# Option A: Using Caddy (recommended) +caddy run --config Caddyfile + +# Option B: Using preview mode +npm run preview + +# Option C: Using static server +npx serve -s dist -l 5173 +``` + +#### Docker Deployment (Recommended) + +```bash +# Build and run with Docker Compose +docker-compose -f docker-compose.app.yml up -d + +# Services will be available at: +# - Frontend: http://localhost:5173 +# - Backend API: http://localhost:8000 +# - Admin: http://localhost:8000/igny8-admin/ +``` + +### Key Differences: Framework Files vs IGNY8 Files + +#### Backend + +**Framework Files (NOT in package):** +- Django's own code (installed via pip) +- Standard Django project structure created by `django-admin startproject` +- Generic migration files for Django's contrib apps +- Python standard library modules + +**IGNY8-Specific Files (IN package):** +- All files in `igny8_core/` with custom business logic +- Custom models, views, serializers, URLs +- AI framework implementation +- Business logic services +- Module-specific code (planner, writer, linker, etc.) +- Custom middleware and utilities +- Database migrations for IGNY8 models +- `settings.py` with IGNY8 configuration +- `requirements.txt` (dependency manifest) + +#### Frontend + +**Framework Files (NOT in package):** +- React library itself (installed via npm) +- node_modules dependencies +- TypeScript compiler +- Vite build tool +- TailwindCSS framework + +**IGNY8-Specific Files (IN package):** +- All `.tsx` and `.ts` files in `src/` +- Custom components, pages, layouts +- State management stores +- API client implementation +- Type definitions for IGNY8 models +- Custom hooks and utilities +- Routing configuration +- Custom styles (`igny8-colors.css`) +- `package.json` (dependency manifest) +- `vite.config.ts` with IGNY8-specific build config + +### Portability Checklist + +✅ **Before packaging:** +- [ ] Remove all `.env` files (environment-specific) +- [ ] Remove database files (`.sqlite3`, `*.sql` backups) +- [ ] Remove logs and temporary files +- [ ] Remove virtual environments (`.venv/`, `node_modules/`) +- [ ] Remove build artifacts (`dist/`, `staticfiles/`, `__pycache__/`) +- [ ] Keep all migration files (critical for database schema) +- [ ] Keep `requirements.txt` and `package.json` (dependency manifests) +- [ ] Keep Dockerfile and docker-compose files + +✅ **After deployment to new infrastructure:** +- [ ] Install dependencies (`pip install -r requirements.txt`, `npm install`) +- [ ] Configure environment variables for new infrastructure +- [ ] Run database migrations +- [ ] Create initial superuser +- [ ] Collect static files (backend) +- [ ] Build frontend assets +- [ ] Configure reverse proxy (Caddy/Nginx) +- [ ] Set up SSL certificates +- [ ] Configure Redis and Celery workers +- [ ] Test all API endpoints +- [ ] Verify WordPress integration works + +### Size Estimation + +**Backend Package:** ~5-10 MB (excluding virtual environment and dependencies) +**Frontend Package:** ~2-5 MB (excluding node_modules) +**Total Application Code:** ~7-15 MB + +**After Installation:** +- Backend with dependencies: ~200-300 MB +- Frontend with dependencies: ~300-500 MB +- Total deployed: ~500-800 MB + +--- + +## Deployment Architecture + +### Production Stack + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Caddy │ +│ (Reverse Proxy + SSL) │ +└─────────────────────┬───────────────────────────────────────────┘ + │ + ┌───────────┴───────────┐ + ▼ ▼ +┌──────────────────┐ ┌──────────────────┐ +│ Frontend (Vite) │ │ Backend (Gunicorn)│ +│ app.igny8.com │ │ api.igny8.com │ +│ Port: 5173 │ │ Port: 8000 │ +└──────────────────┘ └─────────┬──────────┘ + │ + ┌───────────────┴───────────────┐ + ▼ ▼ + ┌──────────────────┐ ┌──────────────────┐ + │ PostgreSQL │ │ Redis │ + │ Database │ │ Celery Broker │ + └──────────────────┘ └─────────┬────────┘ + │ + ▼ + ┌──────────────────┐ + │ Celery Worker │ + │ AI Tasks │ + └──────────────────┘ +``` + +### Docker Compose + +See `docker-compose.app.yml` for full configuration. + +--- + +**End of Master Reference** + +For setup instructions, see `README.md`. +For version history, see `CHANGELOG.md`. +For API details, see `master-docs/API-COMPLETE-REFERENCE.md`. diff --git a/docs/wp/03-WORDPRESS-PLUGIN-API-INTEGRATION-GUIDE.md b/old-docs/wp/03-WORDPRESS-PLUGIN-API-INTEGRATION-GUIDE.md similarity index 100% rename from docs/wp/03-WORDPRESS-PLUGIN-API-INTEGRATION-GUIDE.md rename to old-docs/wp/03-WORDPRESS-PLUGIN-API-INTEGRATION-GUIDE.md diff --git a/docs/wp/04-WORDPRESS-BIDIRECTIONAL-SYNC-REFERENCE.md b/old-docs/wp/04-WORDPRESS-BIDIRECTIONAL-SYNC-REFERENCE.md similarity index 100% rename from docs/wp/04-WORDPRESS-BIDIRECTIONAL-SYNC-REFERENCE.md rename to old-docs/wp/04-WORDPRESS-BIDIRECTIONAL-SYNC-REFERENCE.md diff --git a/docs/wp/DEPLOYMENT-GUIDE-WP-FIXES.md b/old-docs/wp/DEPLOYMENT-GUIDE-WP-FIXES.md similarity index 100% rename from docs/wp/DEPLOYMENT-GUIDE-WP-FIXES.md rename to old-docs/wp/DEPLOYMENT-GUIDE-WP-FIXES.md diff --git a/docs/wp/WORDPRESS-INTEGRATION-FIXES-2025-11-30.md b/old-docs/wp/WORDPRESS-INTEGRATION-FIXES-2025-11-30.md similarity index 100% rename from docs/wp/WORDPRESS-INTEGRATION-FIXES-2025-11-30.md rename to old-docs/wp/WORDPRESS-INTEGRATION-FIXES-2025-11-30.md diff --git a/docs/wp/WORDPRESS-INTEGRATION-FIXES-IMPLEMENTATION-2025-12-01.md b/old-docs/wp/WORDPRESS-INTEGRATION-FIXES-IMPLEMENTATION-2025-12-01.md similarity index 100% rename from docs/wp/WORDPRESS-INTEGRATION-FIXES-IMPLEMENTATION-2025-12-01.md rename to old-docs/wp/WORDPRESS-INTEGRATION-FIXES-IMPLEMENTATION-2025-12-01.md diff --git a/docs/wp/WORDPRESS-INTEGRATION-REFACTOR-PLAN-2025-12-01.md b/old-docs/wp/WORDPRESS-INTEGRATION-REFACTOR-PLAN-2025-12-01.md similarity index 100% rename from docs/wp/WORDPRESS-INTEGRATION-REFACTOR-PLAN-2025-12-01.md rename to old-docs/wp/WORDPRESS-INTEGRATION-REFACTOR-PLAN-2025-12-01.md diff --git a/docs/wp/WORDPRESS-PUBLISHING-FIELD-MAPPING.md b/old-docs/wp/WORDPRESS-PUBLISHING-FIELD-MAPPING.md similarity index 100% rename from docs/wp/WORDPRESS-PUBLISHING-FIELD-MAPPING.md rename to old-docs/wp/WORDPRESS-PUBLISHING-FIELD-MAPPING.md diff --git a/docs/wp/WP-CONTENT-TEMPLATE-IMPLEMENTATION-PLAN.md b/old-docs/wp/WP-CONTENT-TEMPLATE-IMPLEMENTATION-PLAN.md similarity index 100% rename from docs/wp/WP-CONTENT-TEMPLATE-IMPLEMENTATION-PLAN.md rename to old-docs/wp/WP-CONTENT-TEMPLATE-IMPLEMENTATION-PLAN.md diff --git a/docs/wp/WP-PLUGIN-REFACTOR-PLAN.md b/old-docs/wp/WP-PLUGIN-REFACTOR-PLAN.md similarity index 100% rename from docs/wp/WP-PLUGIN-REFACTOR-PLAN.md rename to old-docs/wp/WP-PLUGIN-REFACTOR-PLAN.md diff --git a/docs/wp/WP-PUBLISHING-SIMPLE-REFERENCE.md b/old-docs/wp/WP-PUBLISHING-SIMPLE-REFERENCE.md similarity index 100% rename from docs/wp/WP-PUBLISHING-SIMPLE-REFERENCE.md rename to old-docs/wp/WP-PUBLISHING-SIMPLE-REFERENCE.md diff --git a/docs/wp/WP-REFACTOR-IMPLEMENTATION-SUMMARY.md b/old-docs/wp/WP-REFACTOR-IMPLEMENTATION-SUMMARY.md similarity index 100% rename from docs/wp/WP-REFACTOR-IMPLEMENTATION-SUMMARY.md rename to old-docs/wp/WP-REFACTOR-IMPLEMENTATION-SUMMARY.md