From 1eba4a4e156c70c1fb0119f414ec347a72d60fc5 Mon Sep 17 00:00:00 2001 From: alorig <220087330+alorig@users.noreply.github.com> Date: Fri, 21 Nov 2025 19:18:24 +0500 Subject: [PATCH] wp plugin loaded --- igny8-wp-integration-plugin/.gitattributes | 2 + igny8-wp-integration-plugin/README.md | 367 +++ .../admin/assets/css/admin.css | 347 +++ .../admin/assets/js/admin.js | 178 ++ .../admin/assets/js/post-editor.js | 200 ++ .../admin/class-admin-columns.php | 335 +++ .../admin/class-admin.php | 488 ++++ .../admin/class-post-meta-boxes.php | 469 ++++ .../admin/settings.php | 532 +++++ .../data/link-graph.php | 192 ++ .../data/semantic-mapping.php | 225 ++ .../data/site-collection.php | 579 +++++ .../data/woocommerce.php | 226 ++ .../docs/PHASE_5_IMPLEMENTATION_SUMMARY.md | 185 ++ .../docs/PHASE_6_IMPLEMENTATION_SUMMARY.md | 298 +++ .../docs/STATUS_SYNC_DOCUMENTATION.md | 249 ++ .../docs/STYLE_GUIDE.md | 186 ++ .../docs/VERIFICATION_REPORT_PHASES_1-5.md | 430 ++++ .../docs/WORDPRESS-PLUGIN-INTEGRATION.md | 2084 +++++++++++++++++ .../docs/missing-saas-api-endpoints.md | 27 + .../docs/wp-bridge-implementation-plan.md | 84 + igny8-wp-integration-plugin/igny8-bridge.php | 183 ++ .../includes/class-igny8-api.php | 328 +++ .../includes/class-igny8-link-queue.php | 202 ++ .../includes/class-igny8-rest-api.php | 294 +++ .../includes/class-igny8-site.php | 118 + .../includes/class-igny8-webhook-logs.php | 147 ++ .../includes/class-igny8-webhooks.php | 381 +++ .../includes/functions.php | 605 +++++ .../languages/igny8-bridge.pot | 100 + igny8-wp-integration-plugin/sync/hooks.php | 41 + .../sync/igny8-to-wp.php | 807 +++++++ .../sync/post-sync.php | 363 +++ .../sync/taxonomy-sync.php | 425 ++++ igny8-wp-integration-plugin/uninstall.php | 53 + 35 files changed, 11730 insertions(+) create mode 100644 igny8-wp-integration-plugin/.gitattributes create mode 100644 igny8-wp-integration-plugin/README.md create mode 100644 igny8-wp-integration-plugin/admin/assets/css/admin.css create mode 100644 igny8-wp-integration-plugin/admin/assets/js/admin.js create mode 100644 igny8-wp-integration-plugin/admin/assets/js/post-editor.js create mode 100644 igny8-wp-integration-plugin/admin/class-admin-columns.php create mode 100644 igny8-wp-integration-plugin/admin/class-admin.php create mode 100644 igny8-wp-integration-plugin/admin/class-post-meta-boxes.php create mode 100644 igny8-wp-integration-plugin/admin/settings.php create mode 100644 igny8-wp-integration-plugin/data/link-graph.php create mode 100644 igny8-wp-integration-plugin/data/semantic-mapping.php create mode 100644 igny8-wp-integration-plugin/data/site-collection.php create mode 100644 igny8-wp-integration-plugin/data/woocommerce.php create mode 100644 igny8-wp-integration-plugin/docs/PHASE_5_IMPLEMENTATION_SUMMARY.md create mode 100644 igny8-wp-integration-plugin/docs/PHASE_6_IMPLEMENTATION_SUMMARY.md create mode 100644 igny8-wp-integration-plugin/docs/STATUS_SYNC_DOCUMENTATION.md create mode 100644 igny8-wp-integration-plugin/docs/STYLE_GUIDE.md create mode 100644 igny8-wp-integration-plugin/docs/VERIFICATION_REPORT_PHASES_1-5.md create mode 100644 igny8-wp-integration-plugin/docs/WORDPRESS-PLUGIN-INTEGRATION.md create mode 100644 igny8-wp-integration-plugin/docs/missing-saas-api-endpoints.md create mode 100644 igny8-wp-integration-plugin/docs/wp-bridge-implementation-plan.md create mode 100644 igny8-wp-integration-plugin/igny8-bridge.php create mode 100644 igny8-wp-integration-plugin/includes/class-igny8-api.php create mode 100644 igny8-wp-integration-plugin/includes/class-igny8-link-queue.php create mode 100644 igny8-wp-integration-plugin/includes/class-igny8-rest-api.php create mode 100644 igny8-wp-integration-plugin/includes/class-igny8-site.php create mode 100644 igny8-wp-integration-plugin/includes/class-igny8-webhook-logs.php create mode 100644 igny8-wp-integration-plugin/includes/class-igny8-webhooks.php create mode 100644 igny8-wp-integration-plugin/includes/functions.php create mode 100644 igny8-wp-integration-plugin/languages/igny8-bridge.pot create mode 100644 igny8-wp-integration-plugin/sync/hooks.php create mode 100644 igny8-wp-integration-plugin/sync/igny8-to-wp.php create mode 100644 igny8-wp-integration-plugin/sync/post-sync.php create mode 100644 igny8-wp-integration-plugin/sync/taxonomy-sync.php create mode 100644 igny8-wp-integration-plugin/uninstall.php diff --git a/igny8-wp-integration-plugin/.gitattributes b/igny8-wp-integration-plugin/.gitattributes new file mode 100644 index 00000000..dfe07704 --- /dev/null +++ b/igny8-wp-integration-plugin/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/igny8-wp-integration-plugin/README.md b/igny8-wp-integration-plugin/README.md new file mode 100644 index 00000000..4b91d333 --- /dev/null +++ b/igny8-wp-integration-plugin/README.md @@ -0,0 +1,367 @@ +# IGNY8 WordPress Bridge Plugin + +**Version**: 1.0.0 +**Last Updated**: 2025-10-17 +**Requires**: WordPress 5.0+, PHP 7.4+ + +--- + +## Overview + +The IGNY8 WordPress Bridge Plugin is a **lightweight synchronization interface** that connects WordPress sites to the IGNY8 API. This plugin acts as a bridge, not a content management system, using WordPress native structures (taxonomies, post meta) to sync data bidirectionally with IGNY8. + +### Key Principles + +- ✅ **No Custom Database Tables** - Uses WordPress native taxonomies and post meta +- ✅ **Lightweight Bridge** - Minimal code, maximum efficiency +- ✅ **Two-Way Sync** - WordPress ↔ IGNY8 API synchronization +- ✅ **WordPress Native** - Leverages existing WordPress structures +- ✅ **API-First** - IGNY8 API is the source of truth + +--- + +## Features + +### Core Functionality + +1. **API Authentication** + - Secure token management + - Automatic token refresh + - Encrypted credential storage + +2. **Two-Way Synchronization** + - WordPress → IGNY8: Post status changes sync to IGNY8 tasks + - IGNY8 → WordPress: Content published from IGNY8 creates WordPress posts + +3. **Taxonomy Mapping** + - WordPress taxonomies → IGNY8 Sectors/Clusters + - Hierarchical taxonomies map to IGNY8 Sectors + - Taxonomy terms map to IGNY8 Clusters + +4. **Post Meta Integration** + - `_igny8_task_id` - Links WordPress posts to IGNY8 tasks + - `_igny8_cluster_id` - Links posts to IGNY8 clusters + - `_igny8_sector_id` - Links posts to IGNY8 sectors + - `_igny8_keyword_ids` - Links posts to IGNY8 keywords + +5. **Site Data Collection** + - Automatic collection of WordPress posts, taxonomies, products + - Semantic mapping to IGNY8 structure + - WooCommerce integration support + +6. **Status Mapping** + - WordPress post status → IGNY8 task status + - Automatic sync on post save/publish/status change + +--- + +## Installation + +### Requirements + +- WordPress 5.0 or higher +- PHP 7.4 or higher +- WordPress REST API enabled +- IGNY8 API account credentials + +### Installation Steps + +1. **Download/Clone Plugin** + ```bash + git clone [repository-url] + cd igny8-ai-os + ``` + +2. **Install in WordPress** + - Copy the `igny8-ai-os` folder to `/wp-content/plugins/` + - Or create a symlink for development + +3. **Activate Plugin** + - Go to WordPress Admin → Plugins + - Activate "IGNY8 WordPress Bridge" + +4. **Configure API Connection** + - Go to Settings → IGNY8 API + - Enter your IGNY8 email and password + - Click "Connect to IGNY8" + +--- + +## Configuration + +### API Settings + +Navigate to **Settings → IGNY8 API** to configure: + +- **Email**: Your IGNY8 account email +- **Password**: Your IGNY8 account password +- **Site ID**: Your IGNY8 site ID (auto-detected after connection) + +### WordPress Integration + +The plugin automatically: + +1. **Registers Taxonomies** (if needed): + - `sectors` - Maps to IGNY8 Sectors + - `clusters` - Maps to IGNY8 Clusters + +2. **Registers Post Meta Fields**: + - `_igny8_task_id` + - `_igny8_cluster_id` + - `_igny8_sector_id` + - `_igny8_keyword_ids` + - `_igny8_content_id` + +3. **Sets Up WordPress Hooks**: + - `save_post` - Syncs post changes to IGNY8 + - `publish_post` - Updates keywords on publish + - `transition_post_status` - Handles status changes + +--- + +## Usage + +### Basic Workflow + +#### 1. Connect to IGNY8 API + +```php +// Automatically handled via Settings page +// Or programmatically: +$api = new Igny8API(); +$api->login('your@email.com', 'password'); +``` + +#### 2. Sync WordPress Site Data + +```php +// Collect and send site data to IGNY8 +$site_id = get_option('igny8_site_id'); +igny8_send_site_data_to_igny8($site_id); +``` + +#### 3. WordPress → IGNY8 Sync + +When you save/publish a WordPress post: + +1. Plugin checks for `_igny8_task_id` in post meta +2. If found, syncs post status to IGNY8 task +3. If published, updates related keywords to 'mapped' status + +#### 4. IGNY8 → WordPress Sync + +When content is published from IGNY8: + +1. IGNY8 triggers webhook (or scheduled sync) +2. Plugin creates WordPress post via `wp_insert_post()` +3. Post meta saved with `_igny8_task_id` +4. IGNY8 task updated with WordPress post ID + +--- + +## WordPress Structures Used + +### Taxonomies + +- **`sectors`** (hierarchical) + - Maps to IGNY8 Sectors + - Can be created manually or synced from IGNY8 + +- **`clusters`** (hierarchical) + - Maps to IGNY8 Clusters + - Can be created manually or synced from IGNY8 + +- **Native Taxonomies** + - `category` - Can map to IGNY8 Sectors + - `post_tag` - Can be used for keyword extraction + +### Post Meta Fields + +All stored in WordPress `wp_postmeta` table: + +- `_igny8_task_id` (integer) - IGNY8 task ID +- `_igny8_cluster_id` (integer) - IGNY8 cluster ID +- `_igny8_sector_id` (integer) - IGNY8 sector ID +- `_igny8_keyword_ids` (array) - Array of IGNY8 keyword IDs +- `_igny8_content_id` (integer) - IGNY8 content ID +- `_igny8_last_synced` (datetime) - Last sync timestamp + +### Post Status Mapping + +| WordPress Status | IGNY8 Task Status | +|------------------|-------------------| +| `publish` | `completed` | +| `draft` | `draft` | +| `pending` | `pending` | +| `private` | `completed` | +| `trash` | `archived` | +| `future` | `scheduled` | + +--- + +## API Reference + +### Main Classes + +#### `Igny8API` + +Main API client class for all IGNY8 API interactions. + +```php +$api = new Igny8API(); + +// Login +$api->login('email@example.com', 'password'); + +// Get keywords +$response = $api->get('/planner/keywords/'); + +// Create task +$response = $api->post('/writer/tasks/', $data); + +// Update task +$response = $api->put('/writer/tasks/123/', $data); +``` + +#### `Igny8WordPressSync` + +Handles two-way synchronization between WordPress and IGNY8. + +```php +$sync = new Igny8WordPressSync(); +// Automatically hooks into WordPress post actions +``` + +#### `Igny8SiteIntegration` + +Manages site data collection and semantic mapping. + +```php +$integration = new Igny8SiteIntegration($site_id); +$result = $integration->full_site_scan(); +``` + +### Main Functions + +- `igny8_login($email, $password)` - Authenticate with IGNY8 +- `igny8_sync_post_status_to_igny8($post_id, $post, $update)` - Sync post to IGNY8 +- `igny8_collect_site_data()` - Collect all WordPress site data +- `igny8_send_site_data_to_igny8($site_id)` - Send site data to IGNY8 +- `igny8_map_site_to_semantic_strategy($site_id, $site_data)` - Map to semantic structure + +--- + +## File Structure + +``` +igny8-ai-os/ +├── igny8-bridge.php # Main plugin file +├── README.md # This file +├── docs/ # Documentation hub +│ ├── README.md # Index of available docs +│ ├── WORDPRESS-PLUGIN-INTEGRATION.md +│ ├── wp-bridge-implementation-plan.md +│ ├── missing-saas-api-endpoints.md +│ ├── STATUS_SYNC_DOCUMENTATION.md +│ └── STYLE_GUIDE.md +├── includes/ +│ ├── class-igny8-api.php # API client class +│ ├── class-igny8-sync.php # Sync handler class +│ ├── class-igny8-site.php # Site integration class +│ └── functions.php # Helper functions +├── admin/ +│ ├── class-admin.php # Admin interface +│ ├── settings.php # Settings page +│ └── assets/ +│ ├── css/ +│ └── js/ +├── sync/ +│ ├── hooks.php # WordPress hooks +│ ├── post-sync.php # Post synchronization +│ └── taxonomy-sync.php # Taxonomy synchronization +├── data/ +│ ├── site-collection.php # Site data collection +│ └── semantic-mapping.php # Semantic mapping +└── uninstall.php # Uninstall handler +``` + +--- + +## Development + +### Code Standards + +- Follow WordPress Coding Standards +- Use WordPress native functions +- No custom database tables +- All data in WordPress native structures + +### Testing + +```bash +# Run WordPress unit tests +phpunit + +# Test API connection +wp eval 'var_dump((new Igny8API())->login("test@example.com", "password"));' +``` + +--- + +## Troubleshooting + +### Authentication Issues + +**Problem**: Cannot connect to IGNY8 API + +**Solutions**: +1. Verify email and password are correct +2. Check API endpoint is accessible +3. Check WordPress REST API is enabled +4. Review error logs in WordPress debug log + +### Sync Issues + +**Problem**: Posts not syncing to IGNY8 + +**Solutions**: +1. Verify `_igny8_task_id` exists in post meta +2. Check API token is valid (not expired) +3. Review WordPress hooks are firing +4. Check error logs + +### Token Expiration + +**Problem**: Token expires frequently + +**Solution**: Plugin automatically refreshes tokens. If issues persist, check token refresh logic. + +--- + +## Support + +- **Documentation**: See `WORDPRESS-PLUGIN-INTEGRATION.md` +- **API Documentation**: https://api.igny8.com/docs +- **Issues**: [GitHub Issues](repository-url/issues) + +--- + +## License + +[Your License Here] + +--- + +## Changelog + +### 1.0.0 - 2025-10-17 +- Initial release +- API authentication +- Two-way sync +- Site data collection +- Semantic mapping + +--- + +**Last Updated**: 2025-10-17 + diff --git a/igny8-wp-integration-plugin/admin/assets/css/admin.css b/igny8-wp-integration-plugin/admin/assets/css/admin.css new file mode 100644 index 00000000..fa2ef1ac --- /dev/null +++ b/igny8-wp-integration-plugin/admin/assets/css/admin.css @@ -0,0 +1,347 @@ +/** + * Admin Styles + * + * All styles for IGNY8 Bridge admin interface + * Update this file to change global design + * + * @package Igny8Bridge + */ + +/* ============================================ + Container & Layout + ============================================ */ + +.igny8-settings-container { + max-width: 1200px; +} + +.igny8-settings-card { + background: #fff; + border: 1px solid #ccd0d4; + box-shadow: 0 1px 1px rgba(0,0,0,.04); + padding: 20px; + margin: 20px 0; +} + +.igny8-settings-card h2 { + margin-top: 0; + padding-bottom: 10px; + border-bottom: 1px solid #eee; +} + +/* ============================================ + Status Indicators + ============================================ */ + +.igny8-status-connected { + color: #46b450; + font-weight: bold; +} + +.igny8-status-disconnected { + color: #dc3232; + font-weight: bold; +} + +.igny8-test-result { + margin-left: 10px; +} + +.igny8-test-result .igny8-success { + color: #46b450; +} + +.igny8-test-result .igny8-error { + color: #dc3232; +} + +.igny8-test-result .igny8-loading { + color: #2271b1; +} + +/* ============================================ + Sync Operations + ============================================ */ + +.igny8-sync-actions { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 20px; +} + +.igny8-sync-actions .button { + min-width: 150px; +} + +.igny8-sync-status { + margin-top: 15px; + padding: 10px; + border-radius: 4px; + display: none; +} + +.igny8-sync-status.igny8-sync-status-success { + background-color: #d4edda; + border: 1px solid #c3e6cb; + color: #155724; + display: block; +} + +.igny8-sync-status.igny8-sync-status-error { + background-color: #f8d7da; + border: 1px solid #f5c6cb; + color: #721c24; + display: block; +} + +.igny8-sync-status.igny8-sync-status-loading { + background-color: #d1ecf1; + border: 1px solid #bee5eb; + color: #0c5460; + display: block; +} + +/* ============================================ + Statistics + ============================================ */ + +.igny8-stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 20px; + margin-top: 15px; +} + +.igny8-stat-item { + padding: 15px; + background: #f9f9f9; + border: 1px solid #ddd; + border-radius: 4px; +} + +.igny8-stat-label { + font-size: 12px; + color: #666; + text-transform: uppercase; + margin-bottom: 8px; +} + +.igny8-stat-value { + font-size: 24px; + font-weight: bold; + color: #2271b1; +} + +/* ============================================ + Diagnostics + ============================================ */ + +.igny8-diagnostics-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 16px; + margin-top: 15px; +} + +.igny8-diagnostic-item { + padding: 15px; + background-color: #f6f7f7; + border: 1px solid #dcdcde; + border-radius: 4px; +} + +.igny8-diagnostic-label { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.05em; + color: #555d66; + margin-bottom: 6px; +} + +.igny8-diagnostic-value { + font-size: 18px; + font-weight: 600; + color: #1d2327; +} + +.igny8-diagnostic-item .description { + margin: 6px 0 0; + color: #646970; +} + +/* ============================================ + Buttons + ============================================ */ + +.igny8-button-group { + display: flex; + gap: 10px; + margin: 15px 0; +} + +.igny8-button-group .button { + flex: 1; +} + +/* ============================================ + Loading States + ============================================ */ + +.igny8-loading { + opacity: 0.6; + pointer-events: none; +} + +.igny8-spinner { + display: inline-block; + width: 16px; + height: 16px; + border: 2px solid #f3f3f3; + border-top: 2px solid #2271b1; + border-radius: 50%; + animation: igny8-spin 1s linear infinite; + margin-right: 8px; + vertical-align: middle; +} + +@keyframes igny8-spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* ============================================ + Messages & Notifications + ============================================ */ + +.igny8-message { + padding: 12px; + margin: 15px 0; + border-left: 4px solid; + background: #fff; +} + +.igny8-message.igny8-message-success { + border-color: #46b450; + background-color: #f0f8f0; +} + +.igny8-message.igny8-message-error { + border-color: #dc3232; + background-color: #fff5f5; +} + +.igny8-message.igny8-message-info { + border-color: #2271b1; + background-color: #f0f6fc; +} + +.igny8-message.igny8-message-warning { + border-color: #f0b849; + background-color: #fffbf0; +} + +/* ============================================ + Tables + ============================================ */ + +.igny8-table { + width: 100%; + border-collapse: collapse; + margin: 15px 0; +} + +.igny8-table th, +.igny8-table td { + padding: 10px; + text-align: left; + border-bottom: 1px solid #ddd; +} + +.igny8-table th { + background-color: #f9f9f9; + font-weight: 600; +} + +.igny8-table tr:hover { + background-color: #f9f9f9; +} + +/* ============================================ + Admin Columns + ============================================ */ + +.igny8-badge { + display: inline-block; + padding: 3px 8px; + border-radius: 3px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + line-height: 1.4; +} + +.igny8-badge-igny8 { + background-color: #2271b1; + color: #fff; +} + +.igny8-badge-wordpress { + background-color: #646970; + color: #fff; +} + +.igny8-terms-list { + display: flex; + flex-wrap: wrap; + gap: 4px; +} + +.igny8-term-badge { + display: inline-block; + padding: 2px 6px; + background-color: #f0f0f1; + border: 1px solid #c3c4c7; + border-radius: 2px; + font-size: 11px; + color: #50575e; +} + +.igny8-empty { + color: #a7aaad; + font-style: italic; +} + +.igny8-action-link { + color: #2271b1; + text-decoration: none; + cursor: pointer; +} + +.igny8-action-link:hover { + color: #135e96; + text-decoration: underline; +} + +/* ============================================ + Responsive + ============================================ */ + +@media (max-width: 782px) { + .igny8-sync-actions { + flex-direction: column; + } + + .igny8-sync-actions .button { + width: 100%; + } + + .igny8-stats-grid { + grid-template-columns: 1fr; + } + + .igny8-diagnostics-grid { + grid-template-columns: 1fr; + } +} + diff --git a/igny8-wp-integration-plugin/admin/assets/js/admin.js b/igny8-wp-integration-plugin/admin/assets/js/admin.js new file mode 100644 index 00000000..b6e0f306 --- /dev/null +++ b/igny8-wp-integration-plugin/admin/assets/js/admin.js @@ -0,0 +1,178 @@ +/** + * Admin JavaScript + * + * @package Igny8Bridge + */ + +(function($) { + 'use strict'; + + $(document).ready(function() { + // Test connection button + $('#igny8-test-connection').on('click', function() { + var $button = $(this); + var $result = $('#igny8-test-result'); + + $button.prop('disabled', true).addClass('igny8-loading'); + $result.html('Testing...'); + + $.ajax({ + url: igny8Admin.ajaxUrl, + type: 'POST', + data: { + action: 'igny8_test_connection', + nonce: igny8Admin.nonce + }, + success: function(response) { + if (response.success) { + $result.html('✓ Connection successful'); + } else { + $result.html('✗ ' + (response.data.message || 'Connection failed') + ''); + } + }, + error: function() { + $result.html('✗ Request failed'); + }, + complete: function() { + $button.prop('disabled', false).removeClass('igny8-loading'); + } + }); + }); + + // Sync posts to IGNY8 + $('#igny8-sync-posts').on('click', function() { + igny8TriggerSync('igny8_sync_posts', 'Syncing posts to IGNY8...'); + }); + + // Sync taxonomies + $('#igny8-sync-taxonomies').on('click', function() { + igny8TriggerSync('igny8_sync_taxonomies', 'Syncing taxonomies...'); + }); + + // Sync from IGNY8 + $('#igny8-sync-from-igny8').on('click', function() { + igny8TriggerSync('igny8_sync_from_igny8', 'Syncing from IGNY8...'); + }); + + // Collect and send site data + $('#igny8-collect-site-data').on('click', function() { + igny8TriggerSync('igny8_collect_site_data', 'Collecting and sending site data...'); + }); + + // Load sync statistics + igny8LoadStats(); + + // Handle row action links + $(document).on('click', '.igny8-action-link', function(e) { + e.preventDefault(); + + var $link = $(this); + var postId = $link.data('post-id'); + var action = $link.data('action'); + + if (!postId) { + return; + } + + if (!confirm('Are you sure you want to ' + (action === 'send' ? 'send' : 'update') + ' this post to IGNY8?')) { + return; + } + + $link.text('Processing...').prop('disabled', true); + + $.ajax({ + url: igny8Admin.ajaxUrl, + type: 'POST', + data: { + action: 'igny8_send_to_igny8', + post_id: postId, + action_type: action, + nonce: igny8Admin.nonce + }, + success: function(response) { + if (response.success) { + alert(response.data.message || 'Success!'); + location.reload(); + } else { + alert(response.data.message || 'Failed to send to IGNY8'); + $link.text(action === 'send' ? 'Send to IGNY8' : 'Update in IGNY8').prop('disabled', false); + } + }, + error: function() { + alert('Request failed'); + $link.text(action === 'send' ? 'Send to IGNY8' : 'Update in IGNY8').prop('disabled', false); + } + }); + }); + }); + + /** + * Trigger sync operation + */ + function igny8TriggerSync(action, message) { + var $status = $('#igny8-sync-status'); + var $button = $('#' + action.replace('igny8_', 'igny8-')); + + $status.removeClass('igny8-sync-status-success igny8-sync-status-error') + .addClass('igny8-sync-status-loading') + .html('' + message); + + $button.prop('disabled', true).addClass('igny8-loading'); + + $.ajax({ + url: igny8Admin.ajaxUrl, + type: 'POST', + data: { + action: action, + nonce: igny8Admin.nonce + }, + success: function(response) { + if (response.success) { + $status.removeClass('igny8-sync-status-loading') + .addClass('igny8-sync-status-success') + .html('✓ ' + (response.data.message || 'Operation completed successfully')); + + // Reload stats + igny8LoadStats(); + } else { + $status.removeClass('igny8-sync-status-loading') + .addClass('igny8-sync-status-error') + .html('✗ ' + (response.data.message || 'Operation failed')); + } + }, + error: function() { + $status.removeClass('igny8-sync-status-loading') + .addClass('igny8-sync-status-error') + .html('✗ Request failed'); + }, + complete: function() { + $button.prop('disabled', false).removeClass('igny8-loading'); + } + }); + } + + /** + * Load sync statistics + */ + function igny8LoadStats() { + $.ajax({ + url: igny8Admin.ajaxUrl, + type: 'POST', + data: { + action: 'igny8_get_stats', + nonce: igny8Admin.nonce + }, + success: function(response) { + if (response.success && response.data) { + if (response.data.synced_posts !== undefined) { + $('#igny8-stat-posts').text(response.data.synced_posts); + } + if (response.data.last_sync) { + $('#igny8-stat-last-sync').text(response.data.last_sync); + } + } + } + }); + } +})(jQuery); + diff --git a/igny8-wp-integration-plugin/admin/assets/js/post-editor.js b/igny8-wp-integration-plugin/admin/assets/js/post-editor.js new file mode 100644 index 00000000..2be154c1 --- /dev/null +++ b/igny8-wp-integration-plugin/admin/assets/js/post-editor.js @@ -0,0 +1,200 @@ +/** + * Post Editor JavaScript + * + * Handles AJAX interactions for Planner and Optimizer meta boxes + * + * @package Igny8Bridge + */ + +(function($) { + 'use strict'; + + $(document).ready(function() { + // Fetch Planner Brief + $('#igny8-fetch-brief').on('click', function() { + var $button = $(this); + var $message = $('#igny8-planner-brief-message'); + var postId = $button.data('post-id'); + var taskId = $button.data('task-id'); + + $button.prop('disabled', true).text('Fetching...'); + $message.hide().removeClass('notice-success notice-error'); + + $.ajax({ + url: igny8PostEditor.ajaxUrl, + type: 'POST', + data: { + action: 'igny8_fetch_planner_brief', + nonce: igny8PostEditor.nonce, + post_id: postId, + task_id: taskId + }, + success: function(response) { + if (response.success) { + $message.addClass('notice notice-success inline') + .html('

' + response.data.message + '

') + .show(); + + // Reload page to show updated brief + setTimeout(function() { + location.reload(); + }, 1000); + } else { + $message.addClass('notice notice-error inline') + .html('

' + (response.data.message || 'Failed to fetch brief') + '

') + .show(); + $button.prop('disabled', false).text('Fetch Brief'); + } + }, + error: function() { + $message.addClass('notice notice-error inline') + .html('

Request failed

') + .show(); + $button.prop('disabled', false).text('Fetch Brief'); + } + }); + }); + + // Refresh Planner Task + $('#igny8-refresh-task').on('click', function() { + var $button = $(this); + var $message = $('#igny8-planner-brief-message'); + var postId = $button.data('post-id'); + var taskId = $button.data('task-id'); + + if (!confirm('Are you sure you want to request a refresh of this task from IGNY8 Planner?')) { + return; + } + + $button.prop('disabled', true).text('Requesting...'); + $message.hide().removeClass('notice-success notice-error'); + + $.ajax({ + url: igny8PostEditor.ajaxUrl, + type: 'POST', + data: { + action: 'igny8_refresh_planner_task', + nonce: igny8PostEditor.nonce, + post_id: postId, + task_id: taskId + }, + success: function(response) { + if (response.success) { + $message.addClass('notice notice-success inline') + .html('

' + response.data.message + '

') + .show(); + } else { + $message.addClass('notice notice-error inline') + .html('

' + (response.data.message || 'Failed to request refresh') + '

') + .show(); + } + $button.prop('disabled', false).text('Request Refresh'); + }, + error: function() { + $message.addClass('notice notice-error inline') + .html('

Request failed

') + .show(); + $button.prop('disabled', false).text('Request Refresh'); + } + }); + }); + + // Create Optimizer Job + $('#igny8-create-optimizer-job').on('click', function() { + var $button = $(this); + var $message = $('#igny8-optimizer-message'); + var postId = $button.data('post-id'); + var taskId = $button.data('task-id'); + + if (!confirm('Create a new optimizer job for this post?')) { + return; + } + + $button.prop('disabled', true).text('Creating...'); + $message.hide().removeClass('notice-success notice-error'); + + $.ajax({ + url: igny8PostEditor.ajaxUrl, + type: 'POST', + data: { + action: 'igny8_create_optimizer_job', + nonce: igny8PostEditor.nonce, + post_id: postId, + task_id: taskId, + job_type: 'audit', + priority: 'normal' + }, + success: function(response) { + if (response.success) { + $message.addClass('notice notice-success inline') + .html('

' + response.data.message + '

') + .show(); + + // Reload page to show updated status + setTimeout(function() { + location.reload(); + }, 1000); + } else { + $message.addClass('notice notice-error inline') + .html('

' + (response.data.message || 'Failed to create job') + '

') + .show(); + $button.prop('disabled', false).text('Request Optimization'); + } + }, + error: function() { + $message.addClass('notice notice-error inline') + .html('

Request failed

') + .show(); + $button.prop('disabled', false).text('Request Optimization'); + } + }); + }); + + // Check Optimizer Status + $('#igny8-check-optimizer-status').on('click', function() { + var $button = $(this); + var $message = $('#igny8-optimizer-message'); + var postId = $button.data('post-id'); + var jobId = $button.data('job-id'); + + $button.prop('disabled', true).text('Checking...'); + $message.hide().removeClass('notice-success notice-error'); + + $.ajax({ + url: igny8PostEditor.ajaxUrl, + type: 'POST', + data: { + action: 'igny8_get_optimizer_status', + nonce: igny8PostEditor.nonce, + post_id: postId, + job_id: jobId + }, + success: function(response) { + if (response.success) { + $message.addClass('notice notice-success inline') + .html('

Status: ' + response.data.status + '

') + .show(); + + // Reload page to show updated status + setTimeout(function() { + location.reload(); + }, 1000); + } else { + $message.addClass('notice notice-error inline') + .html('

' + (response.data.message || 'Failed to get status') + '

') + .show(); + } + $button.prop('disabled', false).text('Check Status'); + }, + error: function() { + $message.addClass('notice notice-error inline') + .html('

Request failed

') + .show(); + $button.prop('disabled', false).text('Check Status'); + } + }); + }); + }); + +})(jQuery); + diff --git a/igny8-wp-integration-plugin/admin/class-admin-columns.php b/igny8-wp-integration-plugin/admin/class-admin-columns.php new file mode 100644 index 00000000..59f9de5b --- /dev/null +++ b/igny8-wp-integration-plugin/admin/class-admin-columns.php @@ -0,0 +1,335 @@ + $value) { + $new_columns[$key] = $value; + + if ($key === 'title') { + $new_columns['igny8_source'] = __('Source', 'igny8-bridge'); + $new_columns['igny8_sectors'] = __('Sectors', 'igny8-bridge'); + $new_columns['igny8_clusters'] = __('Clusters', 'igny8-bridge'); + } + } + + return $new_columns; + } + + /** + * Render column content + * + * @param string $column_name Column name + * @param int $post_id Post ID + */ + public function render_column_content($column_name, $post_id) { + switch ($column_name) { + case 'igny8_source': + $this->render_source_column($post_id); + break; + + case 'igny8_sectors': + $this->render_sectors_column($post_id); + break; + + case 'igny8_clusters': + $this->render_clusters_column($post_id); + break; + } + } + + /** + * Render source column + * + * @param int $post_id Post ID + */ + private function render_source_column($post_id) { + $task_id = get_post_meta($post_id, '_igny8_task_id', true); + + if ($task_id) { + echo ''; + echo esc_html__('IGNY8', 'igny8-bridge'); + echo ''; + } else { + echo ''; + echo esc_html__('WP', 'igny8-bridge'); + echo ''; + } + } + + /** + * Render sectors column + * + * @param int $post_id Post ID + */ + private function render_sectors_column($post_id) { + $sectors = wp_get_post_terms($post_id, 'igny8_sectors', array('fields' => 'names')); + + if (!empty($sectors) && !is_wp_error($sectors)) { + echo '
'; + foreach ($sectors as $sector) { + echo '' . esc_html($sector) . ''; + } + echo '
'; + } else { + echo ''; + } + } + + /** + * Render clusters column + * + * @param int $post_id Post ID + */ + private function render_clusters_column($post_id) { + $clusters = wp_get_post_terms($post_id, 'igny8_clusters', array('fields' => 'names')); + + if (!empty($clusters) && !is_wp_error($clusters)) { + echo '
'; + foreach ($clusters as $cluster) { + echo '' . esc_html($cluster) . ''; + } + echo '
'; + } else { + echo ''; + } + } + + /** + * Make columns sortable + * + * @param array $columns Sortable columns + * @return array Modified columns + */ + public function make_columns_sortable($columns) { + $columns['igny8_source'] = 'igny8_source'; + return $columns; + } + + /** + * Add row actions + * + * @param array $actions Existing actions + * @param WP_Post $post Post object + * @return array Modified actions + */ + public function add_row_actions($actions, $post) { + // Only add for published posts + if ($post->post_status !== 'publish') { + return $actions; + } + + // Check if already synced to IGNY8 + $task_id = get_post_meta($post->ID, '_igny8_task_id', true); + + if ($task_id) { + // Already synced - show update action + $actions['igny8_update'] = sprintf( + '%s', + '#', + $post->ID, + __('Update in IGNY8', 'igny8-bridge') + ); + } else { + // Not synced - show send action + $actions['igny8_send'] = sprintf( + '%s', + '#', + $post->ID, + __('Send to IGNY8', 'igny8-bridge') + ); + } + + return $actions; + } + + /** + * Send post to IGNY8 (AJAX handler) + */ + public static function send_to_igny8() { + check_ajax_referer('igny8_admin_nonce', 'nonce'); + + if (!current_user_can('edit_posts')) { + wp_send_json_error(array('message' => 'Unauthorized')); + } + + $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0; + $action = isset($_POST['action_type']) ? sanitize_text_field($_POST['action_type']) : 'send'; + + if (!$post_id) { + wp_send_json_error(array('message' => 'Invalid post ID')); + } + + $post = get_post($post_id); + if (!$post) { + wp_send_json_error(array('message' => 'Post not found')); + } + + if (!igny8_is_connection_enabled()) { + wp_send_json_error(array('message' => 'Connection is disabled. Enable sync operations first.')); + } + + $api = new Igny8API(); + + if (!$api->is_authenticated()) { + wp_send_json_error(array('message' => 'Not authenticated with IGNY8')); + } + + $site_id = get_option('igny8_site_id'); + if (!$site_id) { + wp_send_json_error(array('message' => 'Site ID not set')); + } + + // Prepare post data for IGNY8 + $post_data = array( + 'title' => $post->post_title, + 'content' => $post->post_content, + 'excerpt' => $post->post_excerpt, + 'status' => $post->post_status === 'publish' ? 'completed' : 'draft', + 'post_type' => $post->post_type, + 'url' => get_permalink($post_id), + 'wordpress_post_id' => $post_id + ); + + // Get categories + $categories = wp_get_post_categories($post_id, array('fields' => 'names')); + if (!empty($categories)) { + $post_data['categories'] = $categories; + } + + // Get tags + $tags = wp_get_post_tags($post_id, array('fields' => 'names')); + if (!empty($tags)) { + $post_data['tags'] = $tags; + } + + // Get featured image + $featured_image_id = get_post_thumbnail_id($post_id); + if ($featured_image_id) { + $post_data['featured_image'] = wp_get_attachment_image_url($featured_image_id, 'full'); + } + + // Get sectors and clusters + $sectors = wp_get_post_terms($post_id, 'igny8_sectors', array('fields' => 'ids')); + $clusters = wp_get_post_terms($post_id, 'igny8_clusters', array('fields' => 'ids')); + + if (!empty($sectors)) { + // Get IGNY8 sector IDs from term meta + $igny8_sector_ids = array(); + foreach ($sectors as $term_id) { + $igny8_sector_id = get_term_meta($term_id, '_igny8_sector_id', true); + if ($igny8_sector_id) { + $igny8_sector_ids[] = $igny8_sector_id; + } + } + if (!empty($igny8_sector_ids)) { + $post_data['sector_id'] = $igny8_sector_ids[0]; // Use first sector + } + } + + if (!empty($clusters)) { + // Get IGNY8 cluster IDs from term meta + $igny8_cluster_ids = array(); + foreach ($clusters as $term_id) { + $igny8_cluster_id = get_term_meta($term_id, '_igny8_cluster_id', true); + if ($igny8_cluster_id) { + $igny8_cluster_ids[] = $igny8_cluster_id; + } + } + if (!empty($igny8_cluster_ids)) { + $post_data['cluster_id'] = $igny8_cluster_ids[0]; // Use first cluster + } + } + + // Check if post already has task ID + $existing_task_id = get_post_meta($post_id, '_igny8_task_id', true); + + if ($existing_task_id && $action === 'update') { + // Update existing task + $response = $api->put("/writer/tasks/{$existing_task_id}/", $post_data); + } else { + // Create new task + $response = $api->post("/writer/tasks/", $post_data); + } + + if ($response['success']) { + $task_id = $response['data']['id'] ?? $existing_task_id; + + // Store task ID + update_post_meta($post_id, '_igny8_task_id', $task_id); + update_post_meta($post_id, '_igny8_last_synced', current_time('mysql')); + + wp_send_json_success(array( + 'message' => $action === 'update' ? 'Post updated in IGNY8' : 'Post sent to IGNY8', + 'task_id' => $task_id + )); + } else { + wp_send_json_error(array( + 'message' => 'Failed to send to IGNY8: ' . ($response['error'] ?? 'Unknown error') + )); + } + } +} + +// Initialize +new Igny8AdminColumns(); + +// Register AJAX handler +add_action('wp_ajax_igny8_send_to_igny8', array('Igny8AdminColumns', 'send_to_igny8')); + diff --git a/igny8-wp-integration-plugin/admin/class-admin.php b/igny8-wp-integration-plugin/admin/class-admin.php new file mode 100644 index 00000000..423ff9f7 --- /dev/null +++ b/igny8-wp-integration-plugin/admin/class-admin.php @@ -0,0 +1,488 @@ + 'boolean', + 'sanitize_callback' => array($this, 'sanitize_boolean'), + 'default' => 1 + )); + + register_setting('igny8_bridge_controls', 'igny8_enabled_post_types', array( + 'type' => 'array', + 'sanitize_callback' => array($this, 'sanitize_post_types'), + 'default' => array_keys(igny8_get_supported_post_types()) + )); + + register_setting('igny8_bridge_controls', 'igny8_enable_woocommerce', array( + 'type' => 'boolean', + 'sanitize_callback' => array($this, 'sanitize_boolean'), + 'default' => class_exists('WooCommerce') ? 1 : 0 + )); + + register_setting('igny8_bridge_controls', 'igny8_control_mode', array( + 'type' => 'string', + 'sanitize_callback' => array($this, 'sanitize_control_mode'), + 'default' => 'mirror' + )); + + register_setting('igny8_bridge_controls', 'igny8_enabled_modules', array( + 'type' => 'array', + 'sanitize_callback' => array($this, 'sanitize_modules'), + 'default' => array_keys(igny8_get_available_modules()) + )); + } + + /** + * Enqueue admin scripts and styles + * + * @param string $hook Current admin page hook + */ + public function enqueue_scripts($hook) { + // Enqueue on settings page + if ($hook === 'settings_page_igny8-settings') { + wp_enqueue_style( + 'igny8-admin-style', + IGNY8_BRIDGE_PLUGIN_URL . 'admin/assets/css/admin.css', + array(), + IGNY8_BRIDGE_VERSION + ); + + wp_enqueue_script( + 'igny8-admin-script', + IGNY8_BRIDGE_PLUGIN_URL . 'admin/assets/js/admin.js', + array('jquery'), + IGNY8_BRIDGE_VERSION, + true + ); + + wp_localize_script('igny8-admin-script', 'igny8Admin', array( + 'ajaxUrl' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('igny8_admin_nonce'), + )); + } + + // Enqueue on post/page/product list pages + if (strpos($hook, 'edit.php') !== false) { + $screen = get_current_screen(); + if ($screen && in_array($screen->post_type, array('post', 'page', 'product', ''))) { + wp_enqueue_style( + 'igny8-admin-style', + IGNY8_BRIDGE_PLUGIN_URL . 'admin/assets/css/admin.css', + array(), + IGNY8_BRIDGE_VERSION + ); + + wp_enqueue_script( + 'igny8-admin-script', + IGNY8_BRIDGE_PLUGIN_URL . 'admin/assets/js/admin.js', + array('jquery'), + IGNY8_BRIDGE_VERSION, + true + ); + + wp_localize_script('igny8-admin-script', 'igny8Admin', array( + 'ajaxUrl' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('igny8_admin_nonce'), + )); + } + } + } + + /** + * Render settings page + */ + public function render_settings_page() { + // Handle form submission + if (isset($_POST['igny8_connect']) && check_admin_referer('igny8_settings_nonce')) { + $this->handle_connection(); + } + + // Handle webhook secret regeneration + if (isset($_POST['igny8_regenerate_secret']) && check_admin_referer('igny8_regenerate_secret')) { + $new_secret = igny8_regenerate_webhook_secret(); + add_settings_error( + 'igny8_settings', + 'igny8_secret_regenerated', + __('Webhook secret regenerated. Update it in your IGNY8 SaaS app settings.', 'igny8-bridge'), + 'updated' + ); + } + + // Include settings template + include IGNY8_BRIDGE_PLUGIN_DIR . 'admin/settings.php'; + } + + /** + * Handle API connection + */ + private function handle_connection() { + $email = sanitize_email($_POST['igny8_email'] ?? ''); + $password = $_POST['igny8_password'] ?? ''; + + if (empty($email) || empty($password)) { + add_settings_error( + 'igny8_settings', + 'igny8_error', + __('Email and password are required.', 'igny8-bridge'), + 'error' + ); + return; + } + + $api = new Igny8API(); + + if ($api->login($email, $password)) { + update_option('igny8_email', $email); + + // Try to get site ID (if available) + $site_response = $api->get('/system/sites/'); + if ($site_response['success'] && !empty($site_response['results'])) { + $site = $site_response['results'][0]; + update_option('igny8_site_id', $site['id']); + } + + add_settings_error( + 'igny8_settings', + 'igny8_connected', + __('Successfully connected to IGNY8 API.', 'igny8-bridge'), + 'updated' + ); + } else { + add_settings_error( + 'igny8_settings', + 'igny8_error', + __('Failed to connect to IGNY8 API. Please check your credentials.', 'igny8-bridge'), + 'error' + ); + } + } + + /** + * Test API connection (AJAX handler) + */ + public static function test_connection() { + check_ajax_referer('igny8_admin_nonce', 'nonce'); + + if (!current_user_can('manage_options')) { + wp_send_json_error(array('message' => 'Unauthorized')); + } + + if (!igny8_is_connection_enabled()) { + wp_send_json_error(array('message' => 'Connection is disabled. Enable sync operations to test.')); + } + + $api = new Igny8API(); + + if (!$api->is_authenticated()) { + wp_send_json_error(array('message' => 'Not authenticated')); + } + + // Test with a simple API call + $response = $api->get('/planner/keywords/?page_size=1'); + + if ($response['success']) { + $checked_at = current_time('timestamp'); + update_option('igny8_last_api_health_check', $checked_at); + wp_send_json_success(array( + 'message' => 'Connection successful', + 'data' => $response, + 'checked_at' => $checked_at + )); + } else { + wp_send_json_error(array( + 'message' => 'Connection failed', + 'error' => $response['error'] + )); + } + } + + /** + * Sync posts to IGNY8 (AJAX handler) + */ + public static function sync_posts() { + check_ajax_referer('igny8_admin_nonce', 'nonce'); + + if (!current_user_can('manage_options')) { + wp_send_json_error(array('message' => 'Unauthorized')); + } + + if (!igny8_is_connection_enabled()) { + wp_send_json_error(array('message' => 'Connection is disabled. Enable sync operations first.')); + } + + $result = igny8_batch_sync_post_statuses(); + + wp_send_json_success(array( + 'message' => sprintf('Synced %d posts, %d failed', $result['synced'], $result['failed']), + 'data' => $result + )); + } + + /** + * Sync taxonomies (AJAX handler) + */ + public static function sync_taxonomies() { + check_ajax_referer('igny8_admin_nonce', 'nonce'); + + if (!current_user_can('manage_options')) { + wp_send_json_error(array('message' => 'Unauthorized')); + } + + if (!igny8_is_connection_enabled()) { + wp_send_json_error(array('message' => 'Connection is disabled. Enable sync operations first.')); + } + + $api = new Igny8API(); + if (!$api->is_authenticated()) { + wp_send_json_error(array('message' => 'Not authenticated')); + } + + // Sync sectors and clusters from IGNY8 + $sectors_result = igny8_sync_igny8_sectors_to_wp(); + $clusters_result = igny8_sync_igny8_clusters_to_wp(); + + wp_send_json_success(array( + 'message' => sprintf('Synced %d sectors, %d clusters', + $sectors_result['synced'] ?? 0, + $clusters_result['synced'] ?? 0 + ), + 'data' => array( + 'sectors' => $sectors_result, + 'clusters' => $clusters_result + ) + )); + } + + /** + * Sync from IGNY8 (AJAX handler) + */ + public static function sync_from_igny8() { + check_ajax_referer('igny8_admin_nonce', 'nonce'); + + if (!current_user_can('manage_options')) { + wp_send_json_error(array('message' => 'Unauthorized')); + } + + if (!igny8_is_connection_enabled()) { + wp_send_json_error(array('message' => 'Connection is disabled. Enable sync operations first.')); + } + + $result = igny8_sync_igny8_tasks_to_wp(); + + if ($result['success']) { + wp_send_json_success(array( + 'message' => sprintf('Created %d posts, updated %d posts', + $result['created'], + $result['updated'] + ), + 'data' => $result + )); + } else { + wp_send_json_error(array( + 'message' => $result['error'] ?? 'Sync failed' + )); + } + } + + /** + * Collect and send site data (AJAX handler) + */ + public static function collect_site_data() { + check_ajax_referer('igny8_admin_nonce', 'nonce'); + + if (!current_user_can('manage_options')) { + wp_send_json_error(array('message' => 'Unauthorized')); + } + + if (!igny8_is_connection_enabled()) { + wp_send_json_error(array('message' => 'Connection is disabled. Enable sync operations first.')); + } + + $site_id = get_option('igny8_site_id'); + if (!$site_id) { + wp_send_json_error(array('message' => 'Site ID not set')); + } + + $result = igny8_send_site_data_to_igny8($site_id); + + if ($result) { + wp_send_json_success(array( + 'message' => 'Site data collected and sent successfully', + 'data' => $result + )); + } else { + wp_send_json_error(array('message' => 'Failed to send site data')); + } + } + + /** + * Get sync statistics (AJAX handler) + */ + public static function get_stats() { + check_ajax_referer('igny8_admin_nonce', 'nonce'); + + if (!current_user_can('manage_options')) { + wp_send_json_error(array('message' => 'Unauthorized')); + } + + global $wpdb; + + // Count synced posts + $synced_posts = $wpdb->get_var(" + SELECT COUNT(DISTINCT post_id) + FROM {$wpdb->postmeta} + WHERE meta_key = '_igny8_task_id' + "); + + // Get last sync time + $last_sync = get_option('igny8_last_site_sync', 0); + $last_sync_formatted = $last_sync ? date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $last_sync) : 'Never'; + + wp_send_json_success(array( + 'synced_posts' => intval($synced_posts), + 'last_sync' => $last_sync_formatted + )); + } + + /** + * Sanitize post types option + * + * @param mixed $value Raw value + * @return array + */ + public function sanitize_post_types($value) { + $supported = array_keys(igny8_get_supported_post_types()); + + if (!is_array($value)) { + return $supported; + } + + $clean = array(); + foreach ($value as $post_type) { + $post_type = sanitize_key($post_type); + if (in_array($post_type, $supported, true)) { + $clean[] = $post_type; + } + } + + return !empty($clean) ? $clean : $supported; + } + + /** + * Sanitize boolean option + * + * @param mixed $value Raw value + * @return int + */ + public function sanitize_boolean($value) { + return $value ? 1 : 0; + } + + /** + * Sanitize control mode + * + * @param mixed $value Raw value + * @return string + */ + public function sanitize_control_mode($value) { + $value = is_string($value) ? strtolower($value) : 'mirror'; + return in_array($value, array('mirror', 'hybrid'), true) ? $value : 'mirror'; + } + + /** + * Sanitize module toggles + * + * @param mixed $value Raw value + * @return array + */ + public function sanitize_modules($value) { + $supported = array_keys(igny8_get_available_modules()); + + if (!is_array($value)) { + return $supported; + } + + $clean = array(); + foreach ($value as $module) { + $module = sanitize_key($module); + if (in_array($module, $supported, true)) { + $clean[] = $module; + } + } + + return !empty($clean) ? $clean : $supported; + } +} + +// Register AJAX handlers +add_action('wp_ajax_igny8_test_connection', array('Igny8Admin', 'test_connection')); +add_action('wp_ajax_igny8_sync_posts', array('Igny8Admin', 'sync_posts')); +add_action('wp_ajax_igny8_sync_taxonomies', array('Igny8Admin', 'sync_taxonomies')); +add_action('wp_ajax_igny8_sync_from_igny8', array('Igny8Admin', 'sync_from_igny8')); +add_action('wp_ajax_igny8_collect_site_data', array('Igny8Admin', 'collect_site_data')); +add_action('wp_ajax_igny8_get_stats', array('Igny8Admin', 'get_stats')); + diff --git a/igny8-wp-integration-plugin/admin/class-post-meta-boxes.php b/igny8-wp-integration-plugin/admin/class-post-meta-boxes.php new file mode 100644 index 00000000..d0d6ebcb --- /dev/null +++ b/igny8-wp-integration-plugin/admin/class-post-meta-boxes.php @@ -0,0 +1,469 @@ + admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('igny8_post_editor_nonce'), + )); + } + + /** + * Render Planner Brief meta box + */ + public function render_planner_brief_box($post) { + $task_id = get_post_meta($post->ID, '_igny8_task_id', true); + $brief = get_post_meta($post->ID, '_igny8_task_brief', true); + $brief_cached_at = get_post_meta($post->ID, '_igny8_brief_cached_at', true); + $cluster_id = get_post_meta($post->ID, '_igny8_cluster_id', true); + + if (!$task_id && !$cluster_id) { + echo '

'; + _e('This post is not linked to an IGNY8 task or cluster.', 'igny8-bridge'); + echo '

'; + return; + } + + wp_nonce_field('igny8_post_editor_nonce', 'igny8_post_editor_nonce'); + ?> +
+ +
+ + +

+ + + +
+ +
+ + + +
+ + +
    + +
  • + +
+ +

+ +
+ + + +
+ + '; + foreach ($keywords as $keyword) { + echo '' . esc_html(trim($keyword)) . ''; + } + echo ''; + ?> +
+ + + +
+ + +
+ + +

+ + + +

+ +

+ +
+ +

+ +

+ +
+ +

+ + + + + +

+ + + ID, '_igny8_task_id', true); + $optimizer_job_id = get_post_meta($post->ID, '_igny8_optimizer_job_id', true); + $optimizer_status = get_post_meta($post->ID, '_igny8_optimizer_status', true); + + if (!$task_id) { + echo '

'; + _e('This post is not linked to an IGNY8 task.', 'igny8-bridge'); + echo '

'; + return; + } + + wp_nonce_field('igny8_post_editor_nonce', 'igny8_post_editor_nonce'); + ?> +
+ +
+

+ + +

+ + +

+ + + + +

+ + +

+ +

+
+ +

+ +

+ +
+ +

+ +

+ + + 'Unauthorized')); + } + + if (!igny8_is_connection_enabled()) { + wp_send_json_error(array('message' => 'Connection is disabled. Enable sync operations first.')); + } + + $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0; + $task_id = isset($_POST['task_id']) ? intval($_POST['task_id']) : 0; + + if (!$post_id || !$task_id) { + wp_send_json_error(array('message' => 'Invalid post ID or task ID')); + } + + $api = new Igny8API(); + + if (!$api->is_authenticated()) { + wp_send_json_error(array('message' => 'Not authenticated')); + } + + // Try to fetch from Planner first + $response = $api->get("/planner/tasks/{$task_id}/brief/"); + + if (!$response['success']) { + // Fallback to Writer brief + $response = $api->get("/writer/tasks/{$task_id}/brief/"); + } + + if ($response['success'] && !empty($response['data'])) { + update_post_meta($post_id, '_igny8_task_brief', $response['data']); + update_post_meta($post_id, '_igny8_brief_cached_at', current_time('mysql')); + + wp_send_json_success(array( + 'message' => 'Brief fetched successfully', + 'brief' => $response['data'] + )); + } else { + wp_send_json_error(array( + 'message' => 'Failed to fetch brief: ' . ($response['error'] ?? 'Unknown error') + )); + } + } + + /** + * Refresh Planner task (AJAX handler) + */ + public static function refresh_planner_task() { + check_ajax_referer('igny8_post_editor_nonce', 'nonce'); + + if (!current_user_can('edit_posts')) { + wp_send_json_error(array('message' => 'Unauthorized')); + } + + if (!igny8_is_connection_enabled()) { + wp_send_json_error(array('message' => 'Connection is disabled. Enable sync operations first.')); + } + + $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0; + $task_id = isset($_POST['task_id']) ? intval($_POST['task_id']) : 0; + + if (!$post_id || !$task_id) { + wp_send_json_error(array('message' => 'Invalid post ID or task ID')); + } + + $api = new Igny8API(); + + if (!$api->is_authenticated()) { + wp_send_json_error(array('message' => 'Not authenticated')); + } + + $response = $api->post("/planner/tasks/{$task_id}/refresh/", array( + 'wordpress_post_id' => $post_id, + 'reason' => 'reoptimize', + 'notes' => 'Requested refresh from WordPress editor' + )); + + if ($response['success']) { + wp_send_json_success(array( + 'message' => 'Refresh requested successfully', + 'data' => $response['data'] + )); + } else { + wp_send_json_error(array( + 'message' => 'Failed to request refresh: ' . ($response['error'] ?? 'Unknown error') + )); + } + } + + /** + * Create Optimizer job (AJAX handler) + */ + public static function create_optimizer_job() { + check_ajax_referer('igny8_post_editor_nonce', 'nonce'); + + if (!current_user_can('edit_posts')) { + wp_send_json_error(array('message' => 'Unauthorized')); + } + + if (!igny8_is_connection_enabled()) { + wp_send_json_error(array('message' => 'Connection is disabled. Enable sync operations first.')); + } + + $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0; + $task_id = isset($_POST['task_id']) ? intval($_POST['task_id']) : 0; + $job_type = isset($_POST['job_type']) ? sanitize_text_field($_POST['job_type']) : 'audit'; + $priority = isset($_POST['priority']) ? sanitize_text_field($_POST['priority']) : 'normal'; + + if (!$post_id || !$task_id) { + wp_send_json_error(array('message' => 'Invalid post ID or task ID')); + } + + $api = new Igny8API(); + + if (!$api->is_authenticated()) { + wp_send_json_error(array('message' => 'Not authenticated')); + } + + $response = $api->post("/optimizer/jobs/", array( + 'post_id' => $post_id, + 'task_id' => $task_id, + 'job_type' => $job_type, + 'priority' => $priority + )); + + if ($response['success'] && !empty($response['data'])) { + $job_id = $response['data']['id'] ?? $response['data']['job_id'] ?? null; + + if ($job_id) { + update_post_meta($post_id, '_igny8_optimizer_job_id', $job_id); + update_post_meta($post_id, '_igny8_optimizer_status', $response['data']['status'] ?? 'pending'); + update_post_meta($post_id, '_igny8_optimizer_job_created_at', current_time('mysql')); + } + + wp_send_json_success(array( + 'message' => 'Optimizer job created successfully', + 'job_id' => $job_id, + 'data' => $response['data'] + )); + } else { + wp_send_json_error(array( + 'message' => 'Failed to create optimizer job: ' . ($response['error'] ?? 'Unknown error') + )); + } + } + + /** + * Get Optimizer job status (AJAX handler) + */ + public static function get_optimizer_status() { + check_ajax_referer('igny8_post_editor_nonce', 'nonce'); + + if (!current_user_can('edit_posts')) { + wp_send_json_error(array('message' => 'Unauthorized')); + } + + if (!igny8_is_connection_enabled()) { + wp_send_json_error(array('message' => 'Connection is disabled. Enable sync operations first.')); + } + + $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0; + $job_id = isset($_POST['job_id']) ? intval($_POST['job_id']) : 0; + + if (!$post_id || !$job_id) { + wp_send_json_error(array('message' => 'Invalid post ID or job ID')); + } + + $api = new Igny8API(); + + if (!$api->is_authenticated()) { + wp_send_json_error(array('message' => 'Not authenticated')); + } + + $response = $api->get("/optimizer/jobs/{$job_id}/"); + + if ($response['success'] && !empty($response['data'])) { + $status = $response['data']['status'] ?? 'unknown'; + update_post_meta($post_id, '_igny8_optimizer_status', $status); + + if (!empty($response['data']['score_changes'])) { + update_post_meta($post_id, '_igny8_optimizer_score_changes', $response['data']['score_changes']); + } + + if (!empty($response['data']['recommendations'])) { + update_post_meta($post_id, '_igny8_optimizer_recommendations', $response['data']['recommendations']); + } + + wp_send_json_success(array( + 'message' => 'Status retrieved successfully', + 'status' => $status, + 'data' => $response['data'] + )); + } else { + wp_send_json_error(array( + 'message' => 'Failed to get status: ' . ($response['error'] ?? 'Unknown error') + )); + } + } +} + +// Initialize +new Igny8PostMetaBoxes(); + diff --git a/igny8-wp-integration-plugin/admin/settings.php b/igny8-wp-integration-plugin/admin/settings.php new file mode 100644 index 00000000..4cbe9d46 --- /dev/null +++ b/igny8-wp-integration-plugin/admin/settings.php @@ -0,0 +1,532 @@ + 10)); + +?> + +
+

+ + + +
+
+

+ +
+ + + + + + + + + + + +
+ + + +

+ +

+
+ + + +

+ +

+
+ + +
+
+ + +
+

+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ + + +

+ +

+
+ + +
+ +

+ + +

+
+
+

+
+
+
+
+

+ +

+
+
+
+
+

+ +

+
+
+
+
+

+ +

+
+
+
+
+

+ +

+
+
+
+
+

+ +

+
+
+
+
+

+ +

+
+
+
+
+

+ +

+
+
+
+
+

+ +

+
+
+
+
+
+
+
+ +
+

+

+ + + +

+

+ +

+
+ + +
+

+ +
+ + + + + + + + + + + + + + + + + + + +
+ $label) : ?> + +
+ +

+ +

+
+ $module_label) : ?> + +
+ +

+ +

+
+ +
+ +
+ + +

+ +

+ +
+ + +
+ +

+ +

+
+ + +
+

+ + + + + + + + + + +
+ + +

+ +

+
+ + +
+ + +
+

+ +

+
+
+ +
+

+ + +

+ +

+ + + + + + + + + + + + + + + + + + + +
+ +

+ +
+ +
+

+ + + + + + + + + + + + + + + + + + + +
+ + + +
+ +

+ +
+ + +
+

+

+ +

+

+ +

+
+ + +
+

+ + +
+

+
+ +

+
+ + +
+ + + + +
+ +
+
+ +
+

+ +
+
+
+
-
+
+
+
+
-
+
+
+
+ +
+
+ diff --git a/igny8-wp-integration-plugin/data/link-graph.php b/igny8-wp-integration-plugin/data/link-graph.php new file mode 100644 index 00000000..55011eff --- /dev/null +++ b/igny8-wp-integration-plugin/data/link-graph.php @@ -0,0 +1,192 @@ +post_content; + $source_url = get_permalink($post_id); + $site_url = get_site_url(); + $links = array(); + + // Match all anchor tags with href attributes + preg_match_all('/]+href=["\']([^"\']+)["\'][^>]*>(.*?)<\/a>/is', $content, $matches, PREG_SET_ORDER); + + foreach ($matches as $match) { + $href = $match[1]; + $anchor = strip_tags($match[2]); + + // Skip empty anchors + if (empty(trim($anchor))) { + continue; + } + + // Only process internal links + if (strpos($href, $site_url) === 0 || strpos($href, '/') === 0) { + // Convert relative URLs to absolute + if (strpos($href, '/') === 0 && strpos($href, '//') !== 0) { + $href = $site_url . $href; + } + + // Normalize URL (remove trailing slash, fragments, query params for matching) + $target_url = rtrim($href, '/'); + + // Skip if source and target are the same + if ($source_url === $target_url) { + continue; + } + + $links[] = array( + 'source_url' => $source_url, + 'target_url' => $target_url, + 'anchor' => trim($anchor), + 'post_id' => $post_id + ); + } + } + + return $links; +} + +/** + * Extract link graph from all posts + * + * @param array $post_ids Optional array of post IDs to process. If empty, processes all enabled posts. + * @return array Link graph array + */ +function igny8_extract_link_graph($post_ids = array()) { + // Skip if connection is disabled + if (!igny8_is_connection_enabled()) { + return array(); + } + + if (function_exists('igny8_is_module_enabled') && !igny8_is_module_enabled('linker')) { + return array(); + } + + $enabled_post_types = igny8_get_enabled_post_types(); + + if (empty($post_ids)) { + // Get all published posts of enabled types + $query_args = array( + 'post_type' => $enabled_post_types, + 'post_status' => 'publish', + 'posts_per_page' => -1, + 'fields' => 'ids', + 'suppress_filters' => true + ); + + $post_ids = get_posts($query_args); + } + + $link_graph = array(); + $processed = 0; + + foreach ($post_ids as $post_id) { + $links = igny8_extract_post_links($post_id); + + if (!empty($links)) { + $link_graph = array_merge($link_graph, $links); + } + + $processed++; + + // Limit processing to prevent timeout (can be increased or made configurable) + if ($processed >= 1000) { + break; + } + } + + return $link_graph; +} + +/** + * Send link graph to IGNY8 Linker module + * + * @param int $site_id IGNY8 site ID + * @param array $link_graph Link graph array (optional, will extract if not provided) + * @return array|false Response data or false on failure + */ +function igny8_send_link_graph_to_igny8($site_id, $link_graph = null) { + // Skip if connection is disabled + if (!igny8_is_connection_enabled()) { + return false; + } + + if (function_exists('igny8_is_module_enabled') && !igny8_is_module_enabled('linker')) { + return false; + } + + $api = new Igny8API(); + + if (!$api->is_authenticated()) { + return false; + } + + // Extract link graph if not provided + if ($link_graph === null) { + $link_graph = igny8_extract_link_graph(); + } + + if (empty($link_graph)) { + return array('success' => true, 'message' => 'No links found', 'links_count' => 0); + } + + // Send in batches (max 500 links per batch) + $batch_size = 500; + $batches = array_chunk($link_graph, $batch_size); + $total_sent = 0; + $errors = array(); + + foreach ($batches as $batch) { + $response = $api->post("/linker/link-map/", array( + 'site_id' => $site_id, + 'links' => $batch, + 'total_links' => count($link_graph), + 'batch_number' => count($batches) > 1 ? (count($batches) - count($batches) + array_search($batch, $batches) + 1) : 1, + 'total_batches' => count($batches) + )); + + if ($response['success']) { + $total_sent += count($batch); + } else { + $errors[] = $response['error'] ?? 'Unknown error'; + } + } + + if ($total_sent > 0) { + update_option('igny8_last_link_graph_sync', current_time('timestamp')); + update_option('igny8_last_link_graph_count', $total_sent); + + return array( + 'success' => true, + 'links_sent' => $total_sent, + 'total_links' => count($link_graph), + 'batches' => count($batches), + 'errors' => $errors + ); + } + + return false; +} + diff --git a/igny8-wp-integration-plugin/data/semantic-mapping.php b/igny8-wp-integration-plugin/data/semantic-mapping.php new file mode 100644 index 00000000..1aeb58aa --- /dev/null +++ b/igny8-wp-integration-plugin/data/semantic-mapping.php @@ -0,0 +1,225 @@ + false, 'error' => 'Connection disabled'); + } + + $api = new Igny8API(); + + if (!$api->is_authenticated()) { + return array('success' => false, 'error' => 'Not authenticated'); + } + + // Extract semantic structure from site data + $semantic_map = array( + 'sectors' => array(), + 'clusters' => array(), + 'keywords' => array() + ); + + // Map taxonomies to sectors + foreach ($site_data['taxonomies'] as $tax_name => $tax_data) { + if ($tax_data['taxonomy']['hierarchical']) { + // Hierarchical taxonomies (categories) become sectors + $sector = array( + 'name' => $tax_data['taxonomy']['label'], + 'slug' => $tax_data['taxonomy']['name'], + 'description' => $tax_data['taxonomy']['description'], + 'source' => 'wordpress_taxonomy', + 'source_id' => $tax_name + ); + + // Map terms to clusters + $clusters = array(); + foreach ($tax_data['terms'] as $term) { + $clusters[] = array( + 'name' => $term['name'], + 'slug' => $term['slug'], + 'description' => $term['description'], + 'source' => 'wordpress_term', + 'source_id' => $term['id'] + ); + + // Extract keywords from posts in this term + $keywords = igny8_extract_keywords_from_term_posts($term['id'], $tax_name); + $semantic_map['keywords'] = array_merge($semantic_map['keywords'], $keywords); + } + + $sector['clusters'] = $clusters; + $semantic_map['sectors'][] = $sector; + } + } + + // Map WooCommerce product categories to sectors + if (!empty($site_data['product_categories'])) { + $product_sector = array( + 'name' => 'Products', + 'slug' => 'products', + 'description' => 'WooCommerce product categories', + 'source' => 'woocommerce', + 'clusters' => array() + ); + + foreach ($site_data['product_categories'] as $category) { + $product_sector['clusters'][] = array( + 'name' => $category['name'], + 'slug' => $category['slug'], + 'description' => $category['description'], + 'source' => 'woocommerce_category', + 'source_id' => $category['id'] + ); + } + + $semantic_map['sectors'][] = $product_sector; + } + + // Send semantic map to IGNY8 + $response = $api->post("/planner/sites/{$site_id}/semantic-map/", array( + 'semantic_map' => $semantic_map, + 'site_data' => $site_data + )); + + return $response; +} + +/** + * Extract keywords from posts associated with a taxonomy term + * + * @param int $term_id Term ID + * @param string $taxonomy Taxonomy name + * @return array Formatted keywords array + */ +function igny8_extract_keywords_from_term_posts($term_id, $taxonomy) { + $args = array( + 'post_type' => 'any', + 'posts_per_page' => -1, + 'tax_query' => array( + array( + 'taxonomy' => $taxonomy, + 'field' => 'term_id', + 'terms' => $term_id + ) + ) + ); + + $query = new WP_Query($args); + $keywords = array(); + + if ($query->have_posts()) { + while ($query->have_posts()) { + $query->the_post(); + + // Extract keywords from post title and content + $title_words = str_word_count(get_the_title(), 1); + $content_words = str_word_count(strip_tags(get_the_content()), 1); + + // Combine and get unique keywords + $all_words = array_merge($title_words, $content_words); + $unique_words = array_unique(array_map('strtolower', $all_words)); + + // Filter out common words (stop words) + $stop_words = array('the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by'); + $keywords = array_merge($keywords, array_diff($unique_words, $stop_words)); + } + wp_reset_postdata(); + } + + // Format keywords + $formatted_keywords = array(); + foreach (array_unique($keywords) as $keyword) { + if (strlen($keyword) > 3) { // Only keywords longer than 3 characters + $formatted_keywords[] = array( + 'keyword' => $keyword, + 'source' => 'wordpress_post', + 'source_term_id' => $term_id + ); + } + } + + return $formatted_keywords; +} + +/** + * Complete workflow: Fetch site data → Map to semantic strategy → Restructure content + * + * @param int $site_id IGNY8 site ID + * @return array|false Analysis result or false on failure + */ +function igny8_analyze_and_restructure_site($site_id) { + $api = new Igny8API(); + + if (!$api->is_authenticated()) { + return false; + } + + // Step 1: Collect all site data + $site_data = igny8_collect_site_data(); + + // Step 2: Send to IGNY8 for analysis + $analysis_response = $api->post("/system/sites/{$site_id}/analyze/", array( + 'site_data' => $site_data, + 'analysis_type' => 'full_site_restructure' + )); + + if (!$analysis_response['success']) { + return false; + } + + $analysis_id = $analysis_response['data']['analysis_id'] ?? null; + + // Step 3: Map to semantic strategy + $mapping_response = igny8_map_site_to_semantic_strategy($site_id, $site_data); + + if (!$mapping_response['success']) { + return false; + } + + // Step 4: Get restructuring recommendations + $recommendations_response = $api->get("/system/sites/{$site_id}/recommendations/"); + + if (!$recommendations_response['success']) { + return false; + } + + // Get keywords count from mapping response + $keywords_count = 0; + if (isset($mapping_response['data']['keywords'])) { + $keywords_count = count($mapping_response['data']['keywords']); + } + + return array( + 'analysis_id' => $analysis_id, + 'semantic_map' => $mapping_response['data'] ?? null, + 'recommendations' => $recommendations_response['data'] ?? null, + 'site_data_summary' => array( + 'total_posts' => count($site_data['posts']), + 'total_taxonomies' => count($site_data['taxonomies']), + 'total_products' => count($site_data['products'] ?? array()), + 'total_keywords' => $keywords_count + ) + ); +} + diff --git a/igny8-wp-integration-plugin/data/site-collection.php b/igny8-wp-integration-plugin/data/site-collection.php new file mode 100644 index 00000000..e60f90f2 --- /dev/null +++ b/igny8-wp-integration-plugin/data/site-collection.php @@ -0,0 +1,579 @@ + 'publish', + 'after' => null, + 'max_pages' => 5, + ); + $args = wp_parse_args($args, $defaults); + + $post_type_object = get_post_type_object($post_type); + $rest_base = ($post_type_object && !empty($post_type_object->rest_base)) ? $post_type_object->rest_base : $post_type; + + $base_url = sprintf('%s/wp-json/wp/v2/%s', get_site_url(), $rest_base); + + $query_args = array( + 'per_page' => min($per_page, 100), + 'status' => $args['status'], + 'orderby' => 'modified', + 'order' => 'desc', + ); + + if (!empty($args['after'])) { + $query_args['after'] = gmdate('c', $args['after']); + } + + $formatted_posts = array(); + $page = 1; + + do { + $query_args['page'] = $page; + $response = wp_remote_get(add_query_arg($query_args, $base_url)); + + if (is_wp_error($response)) { + break; + } + + $posts = json_decode(wp_remote_retrieve_body($response), true); + + if (!is_array($posts) || empty($posts)) { + break; + } + + foreach ($posts as $post) { + $content = $post['content']['rendered'] ?? ''; + $word_count = str_word_count(strip_tags($content)); + + $formatted_posts[] = array( + 'id' => $post['id'], + 'title' => html_entity_decode($post['title']['rendered'] ?? ''), + 'content' => $content, + 'excerpt' => $post['excerpt']['rendered'] ?? '', + 'status' => $post['status'] ?? 'draft', + 'url' => $post['link'] ?? '', + 'published' => $post['date'] ?? '', + 'modified' => $post['modified'] ?? '', + 'author' => $post['author'] ?? 0, + 'post_type' => $post['type'] ?? $post_type, + 'taxonomies' => array( + 'categories' => $post['categories'] ?? array(), + 'tags' => $post['tags'] ?? array(), + ), + 'meta' => array( + 'word_count' => $word_count, + 'reading_time' => $word_count ? ceil($word_count / 200) : 0, + 'featured_media' => $post['featured_media'] ?? 0, + ) + ); + } + + if (count($posts) < $query_args['per_page']) { + break; + } + + $page++; + } while ($page <= $args['max_pages']); + + return $formatted_posts; +} + +/** + * Fetch all available post types from WordPress + * + * @return array|false Post types array or false on failure + */ +function igny8_fetch_all_post_types() { + $wp_response = wp_remote_get(get_site_url() . '/wp-json/wp/v2/types'); + + if (is_wp_error($wp_response)) { + return false; + } + + $types = json_decode(wp_remote_retrieve_body($wp_response), true); + + if (!is_array($types)) { + return false; + } + + $post_types = array(); + foreach ($types as $type_name => $type_data) { + if ($type_data['public']) { + $post_types[] = array( + 'name' => $type_name, + 'label' => $type_data['name'], + 'description' => $type_data['description'] ?? '', + 'rest_base' => $type_data['rest_base'] ?? $type_name + ); + } + } + + return $post_types; +} + +/** + * Fetch all posts from all post types + * + * @param int $per_page Posts per page + * @return array All posts + */ +function igny8_fetch_all_wordpress_posts($per_page = 100) { + $post_types = igny8_fetch_all_post_types(); + + if (!$post_types) { + return array(); + } + + $all_posts = array(); + foreach ($post_types as $type) { + $posts = igny8_fetch_wordpress_posts($type['name'], $per_page); + if ($posts) { + $all_posts = array_merge($all_posts, $posts); + } + } + + return $all_posts; +} + +/** + * Fetch all taxonomies from WordPress + * + * @return array|false Taxonomies array or false on failure + */ +function igny8_fetch_wordpress_taxonomies() { + $wp_response = wp_remote_get(get_site_url() . '/wp-json/wp/v2/taxonomies'); + + if (is_wp_error($wp_response)) { + return false; + } + + $taxonomies = json_decode(wp_remote_retrieve_body($wp_response), true); + + if (!is_array($taxonomies)) { + return false; + } + + $formatted_taxonomies = array(); + foreach ($taxonomies as $tax_name => $tax_data) { + if ($tax_data['public']) { + $formatted_taxonomies[] = array( + 'name' => $tax_name, + 'label' => $tax_data['name'], + 'description' => $tax_data['description'] ?? '', + 'hierarchical' => $tax_data['hierarchical'], + 'rest_base' => $tax_data['rest_base'] ?? $tax_name, + 'object_types' => $tax_data['types'] ?? array() + ); + } + } + + return $formatted_taxonomies; +} + +/** + * Fetch all terms for a specific taxonomy + * + * @param string $taxonomy Taxonomy name + * @param int $per_page Terms per page + * @return array|false Formatted terms array or false on failure + */ +function igny8_fetch_taxonomy_terms($taxonomy, $per_page = 100) { + $taxonomy_obj = get_taxonomy($taxonomy); + $rest_base = ($taxonomy_obj && !empty($taxonomy_obj->rest_base)) ? $taxonomy_obj->rest_base : $taxonomy; + + $base_url = sprintf('%s/wp-json/wp/v2/%s', get_site_url(), $rest_base); + + $formatted_terms = array(); + $page = 1; + + do { + $response = wp_remote_get(add_query_arg(array( + 'per_page' => min($per_page, 100), + 'page' => $page + ), $base_url)); + + if (is_wp_error($response)) { + break; + } + + $terms = json_decode(wp_remote_retrieve_body($response), true); + + if (!is_array($terms) || empty($terms)) { + break; + } + + foreach ($terms as $term) { + $formatted_terms[] = array( + 'id' => $term['id'], + 'name' => $term['name'], + 'slug' => $term['slug'], + 'description' => $term['description'] ?? '', + 'count' => $term['count'], + 'parent' => $term['parent'] ?? 0, + 'taxonomy' => $taxonomy, + 'url' => $term['link'] ?? '' + ); + } + + if (count($terms) < min($per_page, 100)) { + break; + } + + $page++; + } while (true); + + return $formatted_terms; +} + +/** + * Fetch all terms from all taxonomies + * + * @param int $per_page Terms per page + * @return array All terms organized by taxonomy + */ +function igny8_fetch_all_taxonomy_terms($per_page = 100) { + $taxonomies = igny8_fetch_wordpress_taxonomies(); + + if (!$taxonomies) { + return array(); + } + + $all_terms = array(); + foreach ($taxonomies as $taxonomy) { + $terms = igny8_fetch_taxonomy_terms($taxonomy['rest_base'], $per_page); + if ($terms) { + $all_terms[$taxonomy['name']] = $terms; + } + } + + return $all_terms; +} + +/** + * Collect all WordPress site data for IGNY8 semantic mapping + * + * @return array Complete site data + */ +function igny8_collect_site_data($args = array()) { + // Skip if connection is disabled + if (!igny8_is_connection_enabled()) { + return array('disabled' => true, 'reason' => 'connection_disabled'); + } + + if (function_exists('igny8_is_module_enabled') && !igny8_is_module_enabled('sites')) { + return array('disabled' => true); + } + + $settings = igny8_get_site_scan_settings($args); + + $site_data = array( + 'site_url' => get_site_url(), + 'site_name' => get_bloginfo('name'), + 'site_description' => get_bloginfo('description'), + 'collected_at' => current_time('mysql'), + 'settings' => $settings, + 'posts' => array(), + 'taxonomies' => array(), + 'products' => array(), + 'product_categories' => array(), + 'product_attributes' => array() + ); + + foreach ((array) $settings['post_types'] as $post_type) { + if (!post_type_exists($post_type) || !igny8_is_post_type_enabled($post_type)) { + continue; + } + + $posts = igny8_fetch_wordpress_posts($post_type, $settings['per_page'], array( + 'after' => $settings['since'], + 'status' => 'publish' + )); + + if ($posts) { + $site_data['posts'] = array_merge($site_data['posts'], $posts); + } + } + + $tracked_taxonomies = array('category', 'post_tag', 'igny8_sectors', 'igny8_clusters'); + foreach ($tracked_taxonomies as $taxonomy) { + if (!taxonomy_exists($taxonomy)) { + continue; + } + + $terms = igny8_fetch_taxonomy_terms($taxonomy, 100); + if ($terms) { + $tax_obj = get_taxonomy($taxonomy); + $site_data['taxonomies'][$taxonomy] = array( + 'taxonomy' => array( + 'name' => $taxonomy, + 'label' => $tax_obj ? $tax_obj->label : $taxonomy, + 'description' => $tax_obj->description ?? '', + 'hierarchical' => $tax_obj ? $tax_obj->hierarchical : false, + ), + 'terms' => $terms + ); + } + } + + if (!empty($settings['include_products']) && function_exists('igny8_is_woocommerce_active') && igny8_is_woocommerce_active()) { + require_once IGNY8_BRIDGE_PLUGIN_DIR . 'data/woocommerce.php'; + + $products = igny8_fetch_woocommerce_products(100); + if ($products) { + $site_data['products'] = $products; + } + + $product_categories = igny8_fetch_product_categories(100); + if ($product_categories) { + $site_data['product_categories'] = $product_categories; + } + + $product_attributes = igny8_fetch_product_attributes(); + if ($product_attributes) { + $site_data['product_attributes'] = $product_attributes; + } + } + + // Extract link graph if Linker module is enabled + if (function_exists('igny8_is_module_enabled') && igny8_is_module_enabled('linker')) { + $post_ids = wp_list_pluck($site_data['posts'], 'id'); + $link_graph = igny8_extract_link_graph($post_ids); + + if (!empty($link_graph)) { + $site_data['link_graph'] = $link_graph; + } + } + + $site_data['summary'] = array( + 'posts' => count($site_data['posts']), + 'taxonomies' => count($site_data['taxonomies']), + 'products' => count($site_data['products']), + 'links' => isset($site_data['link_graph']) ? count($site_data['link_graph']) : 0 + ); + + update_option('igny8_last_site_snapshot', array( + 'timestamp' => current_time('timestamp'), + 'summary' => $site_data['summary'] + )); + + return $site_data; +} + +/** + * Send WordPress site data to IGNY8 for semantic strategy mapping + * + * @param int $site_id IGNY8 site ID + * @return array|false Response data or false on failure + */ +function igny8_send_site_data_to_igny8($site_id, $site_data = null, $args = array()) { + // Skip if connection is disabled + if (!igny8_is_connection_enabled()) { + return false; + } + + $api = new Igny8API(); + + if (!$api->is_authenticated()) { + return false; + } + + // Collect all site data if not provided + if (empty($site_data)) { + $site_data = igny8_collect_site_data($args); + } + + if (empty($site_data) || isset($site_data['disabled'])) { + return false; + } + + // Send to IGNY8 API + $response = $api->post("/system/sites/{$site_id}/import/", array( + 'site_data' => $site_data, + 'import_type' => $args['mode'] ?? 'full_site_scan' + )); + + if ($response['success']) { + // Store import ID for tracking + update_option('igny8_last_site_import_id', $response['data']['import_id'] ?? null); + update_option('igny8_last_site_sync', current_time('timestamp')); + + // Send link graph separately to Linker module if available + if (!empty($site_data['link_graph']) && function_exists('igny8_is_module_enabled') && igny8_is_module_enabled('linker')) { + $link_result = igny8_send_link_graph_to_igny8($site_id, $site_data['link_graph']); + if ($link_result) { + error_log(sprintf('IGNY8: Sent %d links to Linker module', $link_result['links_sent'] ?? 0)); + } + } + + return $response['data']; + } else { + error_log("IGNY8: Failed to send site data: " . ($response['error'] ?? 'Unknown error')); + return false; + } +} + +/** + * Sync only changed posts/taxonomies since last sync + * + * @param int $site_id IGNY8 site ID + * @return array|false Sync result or false on failure + */ +function igny8_sync_incremental_site_data($site_id, $settings = array()) { + // Skip if connection is disabled + if (!igny8_is_connection_enabled()) { + return array('synced' => 0, 'message' => 'Connection disabled'); + } + + $api = new Igny8API(); + + if (!$api->is_authenticated()) { + return false; + } + + $settings = igny8_get_site_scan_settings(wp_parse_args($settings, array('mode' => 'incremental'))); + $since = $settings['since'] ?? intval(get_option('igny8_last_site_sync', 0)); + + $formatted_posts = array(); + + foreach ((array) $settings['post_types'] as $post_type) { + if (!post_type_exists($post_type) || !igny8_is_post_type_enabled($post_type)) { + continue; + } + + $query_args = array( + 'post_type' => $post_type, + 'post_status' => array('publish', 'pending', 'draft', 'future'), + 'posts_per_page' => -1, + 'orderby' => 'modified', + 'order' => 'DESC', + 'suppress_filters' => true, + ); + + if ($since) { + $query_args['date_query'] = array( + array( + 'column' => 'post_modified_gmt', + 'after' => gmdate('Y-m-d H:i:s', $since) + ) + ); + } + + $posts = get_posts($query_args); + + foreach ($posts as $post) { + $word_count = str_word_count(strip_tags($post->post_content)); + + $formatted_posts[] = array( + 'id' => $post->ID, + 'title' => get_the_title($post), + 'content' => $post->post_content, + 'status' => $post->post_status, + 'modified' => $post->post_modified_gmt, + 'post_type' => $post->post_type, + 'url' => get_permalink($post), + 'taxonomies' => array( + 'categories' => wp_get_post_terms($post->ID, 'category', array('fields' => 'ids')), + 'tags' => wp_get_post_terms($post->ID, 'post_tag', array('fields' => 'ids')), + ), + 'meta' => array( + 'task_id' => get_post_meta($post->ID, '_igny8_task_id', true), + 'cluster_id' => get_post_meta($post->ID, '_igny8_cluster_id', true), + 'sector_id' => get_post_meta($post->ID, '_igny8_sector_id', true), + 'word_count' => $word_count, + ) + ); + } + } + + if (empty($formatted_posts)) { + return array('synced' => 0, 'message' => 'No changes since last sync'); + } + + $response = $api->post("/system/sites/{$site_id}/sync/", array( + 'posts' => $formatted_posts, + 'sync_type' => 'incremental', + 'last_sync' => $since, + 'post_types' => $settings['post_types'] + )); + + if ($response['success']) { + update_option('igny8_last_site_sync', current_time('timestamp')); + update_option('igny8_last_incremental_site_sync', array( + 'timestamp' => current_time('timestamp'), + 'count' => count($formatted_posts) + )); + + return array( + 'synced' => count($formatted_posts), + 'message' => 'Incremental sync completed' + ); + } + + return false; +} + +/** + * Run a full site scan and semantic mapping + * + * @param int $site_id IGNY8 site ID + * @param array $settings Scan settings + * @return array|false + */ +function igny8_perform_full_site_scan($site_id, $settings = array()) { + $site_data = igny8_collect_site_data($settings); + + if (empty($site_data) || isset($site_data['disabled'])) { + return false; + } + + $import = igny8_send_site_data_to_igny8($site_id, $site_data, array('mode' => 'full_site_scan')); + + if (!$import) { + return false; + } + + update_option('igny8_last_full_site_scan', current_time('timestamp')); + + // Map to semantic strategy (requires Planner module) + if (!function_exists('igny8_is_module_enabled') || igny8_is_module_enabled('planner')) { + $map_response = igny8_map_site_to_semantic_strategy($site_id, $site_data); + if (!empty($map_response['success'])) { + update_option('igny8_last_semantic_map', current_time('timestamp')); + update_option('igny8_last_semantic_map_summary', array( + 'sectors' => count($map_response['data']['sectors'] ?? array()), + 'keywords' => count($map_response['data']['keywords'] ?? array()) + )); + } + } + + // Send link graph to Linker module if available + if (!empty($site_data['link_graph']) && function_exists('igny8_is_module_enabled') && igny8_is_module_enabled('linker')) { + $link_result = igny8_send_link_graph_to_igny8($site_id, $site_data['link_graph']); + if ($link_result) { + error_log(sprintf('IGNY8: Sent %d links to Linker module during full scan', $link_result['links_sent'] ?? 0)); + } + } + + return $import; +} + diff --git a/igny8-wp-integration-plugin/data/woocommerce.php b/igny8-wp-integration-plugin/data/woocommerce.php new file mode 100644 index 00000000..7f01218e --- /dev/null +++ b/igny8-wp-integration-plugin/data/woocommerce.php @@ -0,0 +1,226 @@ + $headers + )); + + if (is_wp_error($wp_response)) { + return false; + } + + $products = json_decode(wp_remote_retrieve_body($wp_response), true); + + if (!is_array($products)) { + return false; + } + + $formatted_products = array(); + foreach ($products as $product) { + $formatted_products[] = array( + 'id' => $product['id'], + 'name' => $product['name'], + 'slug' => $product['slug'], + 'sku' => $product['sku'], + 'type' => $product['type'], + 'status' => $product['status'], + 'description' => $product['description'], + 'short_description' => $product['short_description'], + 'price' => $product['price'], + 'regular_price' => $product['regular_price'], + 'sale_price' => $product['sale_price'], + 'on_sale' => $product['on_sale'], + 'stock_status' => $product['stock_status'], + 'stock_quantity' => $product['stock_quantity'], + 'categories' => $product['categories'] ?? array(), + 'tags' => $product['tags'] ?? array(), + 'images' => $product['images'] ?? array(), + 'attributes' => $product['attributes'] ?? array(), + 'variations' => $product['variations'] ?? array(), + 'url' => $product['permalink'] + ); + } + + return $formatted_products; +} + +/** + * Fetch WooCommerce product categories + * + * @param int $per_page Categories per page + * @return array|false Formatted categories array or false on failure + */ +function igny8_fetch_product_categories($per_page = 100) { + if (!igny8_is_woocommerce_active()) { + return false; + } + + $consumer_key = get_option('woocommerce_api_consumer_key', ''); + $consumer_secret = get_option('woocommerce_api_consumer_secret', ''); + + $headers = array(); + if ($consumer_key && $consumer_secret) { + $headers['Authorization'] = 'Basic ' . base64_encode($consumer_key . ':' . $consumer_secret); + } + + $wp_response = wp_remote_get(sprintf( + '%s/wp-json/wc/v3/products/categories?per_page=%d', + get_site_url(), + $per_page + ), array( + 'headers' => $headers + )); + + if (is_wp_error($wp_response)) { + return false; + } + + $categories = json_decode(wp_remote_retrieve_body($wp_response), true); + + if (!is_array($categories)) { + return false; + } + + $formatted_categories = array(); + foreach ($categories as $category) { + $formatted_categories[] = array( + 'id' => $category['id'], + 'name' => $category['name'], + 'slug' => $category['slug'], + 'description' => $category['description'] ?? '', + 'count' => $category['count'], + 'parent' => $category['parent'] ?? 0, + 'image' => $category['image']['src'] ?? null + ); + } + + return $formatted_categories; +} + +/** + * Fetch WooCommerce product attributes + * + * @return array|false Formatted attributes array or false on failure + */ +function igny8_fetch_product_attributes() { + if (!igny8_is_woocommerce_active()) { + return false; + } + + $consumer_key = get_option('woocommerce_api_consumer_key', ''); + $consumer_secret = get_option('woocommerce_api_consumer_secret', ''); + + $headers = array(); + if ($consumer_key && $consumer_secret) { + $headers['Authorization'] = 'Basic ' . base64_encode($consumer_key . ':' . $consumer_secret); + } + + $wp_response = wp_remote_get( + get_site_url() . '/wp-json/wc/v3/products/attributes', + array( + 'headers' => $headers + ) + ); + + if (is_wp_error($wp_response)) { + return false; + } + + $attributes = json_decode(wp_remote_retrieve_body($wp_response), true); + + if (!is_array($attributes)) { + return false; + } + + $formatted_attributes = array(); + foreach ($attributes as $attribute) { + // Get attribute terms + $terms_response = wp_remote_get(sprintf( + '%s/wp-json/wc/v3/products/attributes/%d/terms', + get_site_url(), + $attribute['id'] + ), array( + 'headers' => $headers + )); + + $terms = array(); + if (!is_wp_error($terms_response)) { + $terms_data = json_decode(wp_remote_retrieve_body($terms_response), true); + if (is_array($terms_data)) { + foreach ($terms_data as $term) { + $terms[] = array( + 'id' => $term['id'], + 'name' => $term['name'], + 'slug' => $term['slug'] + ); + } + } + } + + $formatted_attributes[] = array( + 'id' => $attribute['id'], + 'name' => $attribute['name'], + 'slug' => $attribute['slug'], + 'type' => $attribute['type'], + 'order_by' => $attribute['order_by'], + 'has_archives' => $attribute['has_archives'], + 'terms' => $terms + ); + } + + return $formatted_attributes; +} + diff --git a/igny8-wp-integration-plugin/docs/PHASE_5_IMPLEMENTATION_SUMMARY.md b/igny8-wp-integration-plugin/docs/PHASE_5_IMPLEMENTATION_SUMMARY.md new file mode 100644 index 00000000..eec33324 --- /dev/null +++ b/igny8-wp-integration-plugin/docs/PHASE_5_IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,185 @@ +# Phase 5 Implementation Summary + +## Overview +Phase 5 implementation adds Planner, Linker, and Optimizer module hooks to the WordPress bridge plugin. + +## Implemented Features + +### 5.1 Planner Briefs Display & Refresh Actions ✅ + +**Files Created:** +- `admin/class-post-meta-boxes.php` - Meta boxes for post editor +- `admin/assets/js/post-editor.js` - JavaScript for AJAX interactions + +**Features:** +- **Planner Brief Meta Box** in post editor sidebar + - Displays cached brief with title, content, outline, keywords, and tone + - Shows cache timestamp + - "Fetch Brief" button to load from IGNY8 API + - "Request Refresh" button to trigger Planner refresh + +**API Endpoints Used:** +- `GET /planner/tasks/{id}/brief/` - Fetch Planner brief (with fallback to Writer brief) +- `POST /planner/tasks/{id}/refresh/` - Request Planner task refresh + +**Meta Fields Added:** +- `_igny8_task_brief` - Cached brief data +- `_igny8_brief_cached_at` - Brief cache timestamp + +**AJAX Handlers:** +- `igny8_fetch_planner_brief` - Fetches and caches brief +- `igny8_refresh_planner_task` - Requests Planner refresh + +--- + +### 5.2 Link Graph Export ✅ + +**Files Created:** +- `data/link-graph.php` - Link graph extraction and export + +**Features:** +- **Link Extraction** from post content + - Extracts all internal links (anchor tags) + - Captures source URL, target URL, and anchor text + - Filters to only internal links (same domain) + - Normalizes URLs for consistency + +- **Link Graph Collection** + - Processes all enabled post types + - Extracts links from published posts + - Configurable batch processing (max 1000 posts per run) + +- **Automatic Export During Site Scans** + - Integrated into `igny8_collect_site_data()` + - Included in site data payload + - Also sent separately to Linker module endpoint + +**API Endpoints Used:** +- `POST /linker/link-map/` - Send link graph to Linker module + +**Functions:** +- `igny8_extract_post_links($post_id)` - Extract links from single post +- `igny8_extract_link_graph($post_ids)` - Extract links from multiple posts +- `igny8_send_link_graph_to_igny8($site_id, $link_graph)` - Send to IGNY8 API + +**Integration Points:** +- `igny8_collect_site_data()` - Includes link graph in site data +- `igny8_send_site_data_to_igny8()` - Sends link graph after site import +- `igny8_perform_full_site_scan()` - Sends link graph during full scans + +**Options Stored:** +- `igny8_last_link_graph_sync` - Last sync timestamp +- `igny8_last_link_graph_count` - Number of links sent + +--- + +### 5.4 Optimizer Triggers ✅ + +**Features:** +- **Optimizer Meta Box** in post editor sidebar + - Displays current optimizer job ID and status + - "Request Optimization" button to create new job + - "Check Status" button to fetch latest job status + - Shows score changes and recommendations when available + +**API Endpoints Used:** +- `POST /optimizer/jobs/` - Create optimizer job +- `GET /optimizer/jobs/{id}/` - Get optimizer job status + +**Meta Fields Added:** +- `_igny8_optimizer_job_id` - Optimizer job ID +- `_igny8_optimizer_status` - Job status (pending, processing, completed, failed) +- `_igny8_optimizer_score_changes` - Score changes from optimization +- `_igny8_optimizer_recommendations` - Optimization recommendations +- `_igny8_optimizer_job_created_at` - Job creation timestamp + +**AJAX Handlers:** +- `igny8_create_optimizer_job` - Creates new optimizer job +- `igny8_get_optimizer_status` - Fetches job status and updates meta + +**Job Parameters:** +- `post_id` - WordPress post ID +- `task_id` - IGNY8 task ID +- `job_type` - Type of job (default: 'audit') +- `priority` - Job priority (default: 'normal') + +--- + +## Module Toggle Support + +All Phase 5 features respect the module toggle settings: +- **Planner** module must be enabled for brief fetching/refresh +- **Linker** module must be enabled for link graph export +- **Optimizer** module must be enabled for optimizer jobs + +Features also check `igny8_is_connection_enabled()` to ensure sync operations are active. + +--- + +## UI/UX Features + +### Post Editor Meta Boxes +- Clean, organized display in sidebar +- Real-time AJAX updates +- Success/error message display +- Auto-refresh after operations +- Disabled state when connection is off + +### JavaScript Enhancements +- Loading states on buttons +- Confirmation dialogs for destructive actions +- Error handling and user feedback +- Non-blocking AJAX requests + +--- + +## Integration with Existing Code + +### Modified Files: +- `igny8-bridge.php` - Added meta boxes class loading +- `includes/functions.php` - Added optimizer meta field registrations +- `data/site-collection.php` - Integrated link graph extraction + +### New Dependencies: +- None (uses existing Igny8API class) + +--- + +## Testing Checklist + +- [ ] Planner brief displays correctly in post editor +- [ ] Fetch brief button works and caches data +- [ ] Request refresh button triggers Planner refresh +- [ ] Link graph extraction works on posts with links +- [ ] Link graph is included in site scans +- [ ] Link graph is sent to Linker endpoint +- [ ] Optimizer job creation works +- [ ] Optimizer status check works +- [ ] All features respect module toggles +- [ ] All features respect connection enabled toggle +- [ ] Meta boxes only show for posts with task IDs +- [ ] Error handling works correctly + +--- + +## Notes + +1. **Link Graph Processing**: Currently limited to 1000 posts per run to prevent timeouts. Can be increased or made configurable. + +2. **Brief Caching**: Briefs are cached in post meta to reduce API calls. Cache can be refreshed manually. + +3. **Optimizer Jobs**: Jobs are created asynchronously. Status must be checked manually or via webhook (Phase 6). + +4. **Module Dependencies**: All features check module enablement before executing. + +5. **Connection Toggle**: All features respect the master connection toggle added earlier. + +--- + +## Next Steps (Phase 6) + +Phase 5.3 (Accept link recommendations via webhook) is deferred to Phase 6, which will implement: +- REST endpoint `/wp-json/igny8/v1/event` with shared secret +- Webhook handler for link recommendations +- Link insertion queue system + diff --git a/igny8-wp-integration-plugin/docs/PHASE_6_IMPLEMENTATION_SUMMARY.md b/igny8-wp-integration-plugin/docs/PHASE_6_IMPLEMENTATION_SUMMARY.md new file mode 100644 index 00000000..827c88b9 --- /dev/null +++ b/igny8-wp-integration-plugin/docs/PHASE_6_IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,298 @@ +# Phase 6 Implementation Summary + +## Overview +Phase 6 implementation adds webhook support and remote control capabilities, allowing IGNY8 SaaS to send events to WordPress. + +## Implemented Features + +### 6.1 REST Route Registration with Shared Secret ✅ + +**Files Created:** +- `includes/class-igny8-webhooks.php` - Main webhook handler class + +**Features:** +- **REST Endpoint**: `/wp-json/igny8/v1/event` (POST) +- **Shared Secret Authentication**: HMAC-SHA256 signature verification +- **Connection Check**: All webhook handlers verify `igny8_is_connection_enabled()` before processing + +**Security:** +- Webhook secret stored in WordPress options (auto-generated on first use) +- Signature verification via `X-IGNY8-Signature` header +- Secret can be regenerated from settings page +- Failed authentication attempts are logged + +**Functions:** +- `igny8_get_webhook_secret()` - Get or generate webhook secret +- `igny8_regenerate_webhook_secret()` - Regenerate secret + +--- + +### 6.2 SaaS Event Handlers ✅ + +**Event Types Supported:** + +#### 1. Task Published (`task_published`, `task_completed`) +- Creates or updates WordPress post from IGNY8 task +- Fetches full task data from Writer API +- Respects enabled post types +- Updates post status if task is completed + +#### 2. Link Recommendation (`link_recommendation`, `insert_link`) +- Queues link insertion for processing +- Validates required parameters (post_id, target_url, anchor) +- Respects Linker module toggle +- Processes links asynchronously via cron + +#### 3. Optimizer Request (`optimizer_request`, `optimizer_job_completed`) +- Updates optimizer job status +- Stores score changes and recommendations +- Updates post meta with optimizer data +- Respects Optimizer module toggle + +**Event Handler Flow:** +1. Verify connection is enabled +2. Verify module is enabled (if applicable) +3. Validate event data +4. Process event +5. Log activity +6. Return unified JSON response + +--- + +### 6.3 Webhook Activity Logging ✅ + +**Files Created:** +- `includes/class-igny8-webhook-logs.php` - Logging functions + +**Features:** +- **Log Storage**: WordPress options (last 500 logs) +- **Log Fields**: + - Event type + - Event data + - IP address + - User agent + - Status (received, processed, failed) + - Response data + - Timestamps (received_at, processed_at) + - Error messages + +**Functions:** +- `igny8_log_webhook_activity()` - Log webhook receipt +- `igny8_update_webhook_log()` - Update log with processing result +- `igny8_get_webhook_logs()` - Retrieve logs with filtering +- `igny8_clear_old_webhook_logs()` - Cleanup old logs + +**UI Display:** +- Recent webhook activity table in settings page +- Shows last 10 webhook events +- Displays event type, status, and timestamp + +--- + +### Link Queue System ✅ + +**Files Created:** +- `includes/class-igny8-link-queue.php` - Link insertion queue + +**Features:** +- **Queue Storage**: WordPress options +- **Queue Processing**: Cron-based (processes 10 items per run) +- **Retry Logic**: Up to 3 attempts per link +- **Status Tracking**: pending, completed, failed + +**Link Insertion Logic:** +1. Finds anchor text in post content +2. Wraps anchor with link tag +3. Avoids duplicate links +4. Falls back to appending link if anchor not found + +**Queue Management:** +- Automatic processing via cron +- Manual trigger available +- Queue size limit (1000 items) +- Status tracking per item + +--- + +## Connection Checks + +**All handlers check connection status:** + +✅ **Webhook Handler** (`verify_webhook_secret`) +- Checks `igny8_is_connection_enabled()` before allowing webhook + +✅ **Event Handlers** (`handle_webhook`, `handle_task_published`, etc.) +- Double-checks connection enabled before processing +- Returns error if connection disabled + +✅ **Link Queue** (`igny8_queue_link_insertion`, `igny8_process_link_queue`) +- Checks connection enabled before queuing/processing + +✅ **REST API Endpoints** (existing endpoints) +- Updated to check connection enabled +- Returns 403 if connection disabled + +--- + +## Settings UI Enhancements + +**New Sections Added:** + +1. **Webhook Configuration** + - Webhook URL display with copy button + - Webhook secret display with copy button + - Regenerate secret button + +2. **Link Queue** + - Shows pending links count + - Displays queue table (last 10 items) + - Shows post ID, anchor, target URL, status + +3. **Recent Webhook Activity** + - Shows last 10 webhook events + - Displays event type, status, timestamp + - Color-coded status badges + +--- + +## Security Features + +1. **HMAC Signature Verification** + - Uses SHA-256 HMAC + - Compares request body signature + - Prevents replay attacks + +2. **Connection Toggle Protection** + - All endpoints check connection status + - Webhooks rejected if connection disabled + - Clear error messages + +3. **Module Toggle Respect** + - Events only processed if module enabled + - Graceful error responses + +4. **Input Validation** + - All parameters sanitized + - Required fields validated + - Type checking + +--- + +## API Response Format + +All webhook responses follow unified JSON format: + +**Success:** +```json +{ + "success": true, + "message": "Event processed", + "data": { ... } +} +``` + +**Error:** +```json +{ + "success": false, + "error": "Error message", + "code": "error_code" +} +``` + +--- + +## Integration Points + +### Modified Files: +- `igny8-bridge.php` - Added webhook classes loading +- `includes/functions.php` - Added webhook secret functions +- `includes/class-igny8-rest-api.php` - Added connection checks +- `admin/settings.php` - Added webhook UI sections +- `admin/class-admin.php` - Added secret regeneration handler + +### New Dependencies: +- None (uses existing WordPress functions) + +--- + +## Testing Checklist + +- [ ] Webhook endpoint accessible at `/wp-json/igny8/v1/event` +- [ ] Signature verification works correctly +- [ ] Invalid signatures are rejected +- [ ] Connection disabled blocks webhooks +- [ ] Task published event creates/updates posts +- [ ] Link recommendation queues links +- [ ] Link queue processes links correctly +- [ ] Optimizer events update post meta +- [ ] Webhook logs are created and updated +- [ ] Settings UI displays webhook info +- [ ] Secret regeneration works +- [ ] All events respect module toggles +- [ ] All events respect connection toggle + +--- + +## Notes + +1. **Webhook Secret**: Auto-generated on first use. Must be configured in IGNY8 SaaS app. + +2. **Link Queue**: Processes 10 items per cron run to prevent timeouts. Can be adjusted. + +3. **Log Retention**: Keeps last 500 logs. Older logs can be cleared manually. + +4. **Signature Header**: IGNY8 SaaS must send `X-IGNY8-Signature` header with HMAC-SHA256 signature of request body. + +5. **Connection Toggle**: All webhook handlers check connection status before processing. This ensures no data is processed when connection is disabled. + +6. **Module Toggles**: Each event type checks if its module is enabled before processing. + +--- + +## Webhook Payload Examples + +### Task Published +```json +{ + "event": "task_published", + "data": { + "task_id": 123, + "status": "completed" + } +} +``` + +### Link Recommendation +```json +{ + "event": "link_recommendation", + "data": { + "post_id": 456, + "target_url": "https://example.com/page", + "anchor": "example link", + "priority": "normal" + } +} +``` + +### Optimizer Request +```json +{ + "event": "optimizer_job_completed", + "data": { + "post_id": 456, + "job_id": 789, + "status": "completed", + "score_changes": { ... }, + "recommendations": [ ... ] + } +} +``` + +--- + +## Next Steps + +Phase 6 is complete. All webhook functionality is implemented with proper security, logging, and connection checks. + diff --git a/igny8-wp-integration-plugin/docs/STATUS_SYNC_DOCUMENTATION.md b/igny8-wp-integration-plugin/docs/STATUS_SYNC_DOCUMENTATION.md new file mode 100644 index 00000000..14591a62 --- /dev/null +++ b/igny8-wp-integration-plugin/docs/STATUS_SYNC_DOCUMENTATION.md @@ -0,0 +1,249 @@ +# Status Sync & Content ID Documentation + +**Last Updated**: 2025-10-17 + +--- + +## Overview + +This document explains how the plugin handles status synchronization and content_id tracking between IGNY8 and WordPress. + +--- + +## Status Mapping + +### IGNY8 → WordPress + +When content arrives from IGNY8, status is mapped as follows: + +| IGNY8 Status | WordPress Status | Description | +|--------------|------------------|-------------| +| `completed` | `publish` | Content is published | +| `draft` | `draft` | Content is draft | +| `pending` | `pending` | Content pending review | +| `scheduled` | `future` | Content scheduled | +| `archived` | `trash` | Content archived | + +### WordPress → IGNY8 + +When WordPress post status changes, it's mapped back to IGNY8: + +| WordPress Status | IGNY8 Status | Description | +|------------------|--------------|-------------| +| `publish` | `completed` | Post is published | +| `draft` | `draft` | Post is draft | +| `pending` | `pending` | Post pending review | +| `private` | `completed` | Post is private (published) | +| `trash` | `archived` | Post is deleted | +| `future` | `scheduled` | Post is scheduled | + +--- + +## Content ID Tracking + +### Storage + +The plugin stores IGNY8 `content_id` in post meta: +- **Meta Key**: `_igny8_content_id` +- **Type**: Integer +- **REST API**: Available via `/wp-json/wp/v2/posts?meta_key=_igny8_content_id&meta_value=123` + +### Task ID Tracking + +The plugin also stores IGNY8 `task_id`: +- **Meta Key**: `_igny8_task_id` +- **Type**: Integer +- **REST API**: Available via `/wp-json/wp/v2/posts?meta_key=_igny8_task_id&meta_value=456` + +--- + +## Response to IGNY8 + +When content is created/updated in WordPress, the plugin responds to IGNY8 with: + +```json +{ + "assigned_post_id": 123, + "post_url": "https://example.com/post/", + "wordpress_status": "publish", + "status": "completed", + "synced_at": "2025-10-17 12:00:00", + "post_type": "post", + "content_type": "post", + "content_id": 789 +} +``` + +### Response Fields + +- **`assigned_post_id`**: WordPress post ID +- **`post_url`**: Full URL to the post +- **`wordpress_status`**: Actual WordPress status (publish/pending/draft) +- **`status`**: IGNY8 mapped status (completed/pending/draft) +- **`synced_at`**: Timestamp of sync +- **`post_type`**: WordPress post type +- **`content_type`**: IGNY8 content type +- **`content_id`**: IGNY8 content ID (if provided) + +--- + +## REST API Endpoints + +The plugin provides REST API endpoints for IGNY8 to query WordPress: + +### 1. Get Post by Content ID + +**Endpoint**: `GET /wp-json/igny8/v1/post-by-content-id/{content_id}` + +**Response**: +```json +{ + "success": true, + "data": { + "post_id": 123, + "title": "Post Title", + "status": "publish", + "wordpress_status": "publish", + "igny8_status": "completed", + "url": "https://example.com/post/", + "post_type": "post", + "content_id": 789, + "task_id": 456, + "last_synced": "2025-10-17 12:00:00" + } +} +``` + +### 2. Get Post by Task ID + +**Endpoint**: `GET /wp-json/igny8/v1/post-by-task-id/{task_id}` + +**Response**: Same format as above + +### 3. Get Post Status by Content ID + +**Endpoint**: `GET /wp-json/igny8/v1/post-status/{content_id}` + +**Response**: +```json +{ + "success": true, + "data": { + "post_id": 123, + "wordpress_status": "publish", + "igny8_status": "completed", + "status_mapping": { + "publish": "completed", + "draft": "draft", + "pending": "pending", + "private": "completed", + "trash": "archived", + "future": "scheduled" + }, + "content_id": 789, + "url": "https://example.com/post/", + "last_synced": "2025-10-17 12:00:00" + } +} +``` + +--- + +## Status Flow + +### When Content Arrives from IGNY8 + +1. **Receive** content with `content_type`, `status`, `content_id`, `task_id` +2. **Map** IGNY8 status to WordPress status +3. **Create** WordPress post with mapped status +4. **Store** `content_id` and `task_id` in post meta +5. **Respond** to IGNY8 with: + - WordPress post ID + - WordPress actual status + - IGNY8 mapped status + - Post URL + - Content ID + +### When WordPress Status Changes + +1. **Detect** status change via `save_post` or `transition_post_status` hook +2. **Get** `task_id` and `content_id` from post meta +3. **Map** WordPress status to IGNY8 status +4. **Update** IGNY8 task with: + - WordPress actual status + - IGNY8 mapped status + - Post URL + - Content ID (if available) + - Sync timestamp + +--- + +## Available Meta Fields + +All fields are available for IGNY8 to read via REST API: + +- `_igny8_task_id` - IGNY8 task ID +- `_igny8_content_id` - IGNY8 content ID +- `_igny8_cluster_id` - IGNY8 cluster ID +- `_igny8_sector_id` - IGNY8 sector ID +- `_igny8_keyword_ids` - Array of keyword IDs +- `_igny8_wordpress_status` - WordPress post status +- `_igny8_last_synced` - Last sync timestamp + +--- + +## Query Examples + +### Via WordPress REST API + +```bash +# Get post by content_id +GET /wp-json/wp/v2/posts?meta_key=_igny8_content_id&meta_value=123 + +# Get post by task_id +GET /wp-json/wp/v2/posts?meta_key=_igny8_task_id&meta_value=456 + +# Get all IGNY8 posts +GET /wp-json/wp/v2/posts?meta_key=_igny8_task_id +``` + +### Via IGNY8 REST API Endpoints + +```bash +# Get post by content_id (with status info) +GET /wp-json/igny8/v1/post-by-content-id/123 + +# Get post status only +GET /wp-json/igny8/v1/post-status/123 + +# Get post by task_id +GET /wp-json/igny8/v1/post-by-task-id/456 +``` + +--- + +## Authentication + +REST API endpoints require: +- IGNY8 API authentication (access token) +- Authorization header: `Bearer {access_token}` + +Or internal use when IGNY8 is connected. + +--- + +## Status Flags Available + +✅ **Task ID** - Stored and queryable +✅ **Content ID** - Stored and queryable +✅ **WordPress Status** - Stored and sent to IGNY8 +✅ **IGNY8 Status** - Mapped and sent to IGNY8 +✅ **Post Type** - Stored and sent to IGNY8 +✅ **Content Type** - Stored and sent to IGNY8 +✅ **Sync Timestamp** - Stored and sent to IGNY8 +✅ **Post URL** - Sent to IGNY8 + +--- + +**All status information is available for IGNY8 to read and query!** + diff --git a/igny8-wp-integration-plugin/docs/STYLE_GUIDE.md b/igny8-wp-integration-plugin/docs/STYLE_GUIDE.md new file mode 100644 index 00000000..059cd8ee --- /dev/null +++ b/igny8-wp-integration-plugin/docs/STYLE_GUIDE.md @@ -0,0 +1,186 @@ +# Style Guide - IGNY8 WordPress Bridge + +**Last Updated**: 2025-10-17 + +--- + +## CSS Architecture + +### No Inline CSS Policy + +✅ **All styles are in `admin/assets/css/admin.css`** +❌ **No inline `style=""` attributes** +❌ **No `