Plugin packaging and docs

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-09 22:45:30 +00:00
parent 80f1709a2e
commit 4343f62140
63 changed files with 1369 additions and 223 deletions

View File

@@ -0,0 +1,847 @@
/**
* Admin Styles - IGNY8 Bridge
* Updated with IGNY8 brand colors and modern UI
*
* @package Igny8Bridge
*/
/* ============================================
IGNY8 Brand Colors
============================================ */
:root {
--igny8-primary: #3B82F6;
--igny8-primary-hover: #2563EB;
--igny8-success: #10B981;
--igny8-warning: #F59E0B;
--igny8-error: #EF4444;
--igny8-purple: #8B5CF6;
--igny8-gray: #6B7280;
--igny8-light-gray: #F3F4F6;
}
/* ============================================
Container & Layout
============================================ */
.igny8-settings-container {
width: 95%;
margin: 0 auto;
max-width: none;
box-sizing: border-box;
}
/* On somewhat smaller screens cap at 1200px */
@media (max-width: 1440px) {
.igny8-settings-container {
width: 1200px;
max-width: 100%;
}
}
/* Very small screens: use full width with side padding */
@media (max-width: 1200px) {
.igny8-settings-container {
width: 100%;
padding: 0 16px;
}
}
/* Page header */
.igny8-page-header {
height: 100px;
display: flex;
align-items: center;
border-radius: 8px;
margin: 12px 0 24px 0;
}
.igny8-page-header h1 {
margin: 0;
font-size: 20px;
font-weight: 700;
color: #111827;
}
.igny8-module-title h2 {
margin: 0;
font-size: 18px;
font-weight: 700;
color: #0f172a;
}
/* Ensure top cards equal height */
.igny8-top-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
align-items: stretch;
margin-bottom: 32px;
}
.igny8-top-grid > .igny8-settings-card {
display: flex;
flex-direction: column;
justify-content: flex-start;
height: 100%;
}
/* Make communication primary buttons match API connection buttons */
.igny8-settings-card .button-primary,
.igny8-connection-actions .button-primary {
background: var(--igny8-primary) !important;
border-color: var(--igny8-primary) !important;
color: #fff !important;
border-radius: 8px !important;
padding: 10px 18px !important;
box-shadow: 0 6px 12px rgba(59,130,246,0.08);
}
/* Automation settings UI improvements */
.automation-block label,
.automation-block .description,
.automation-block h3 {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial;
}
.automation-block label {
display: flex;
align-items: center;
gap: 10px;
color: #111827;
font-size: 14px;
line-height: 1.7;
margin-bottom: 6px;
}
.automation-block input[type="checkbox"] {
width: 18px;
height: 18px;
accent-color: var(--igny8-primary);
}
/* Taxonomies list split into two columns inside right automation column */
.taxonomy-list {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px 18px;
align-items: start;
}
.taxonomy-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #111827;
}
.taxonomy-slug {
color: #6B7280;
font-size: 12px;
margin-left: 6px;
}
/* Module header gradient (left-to-right) */
.igny8-module-title {
background: linear-gradient(90deg, #9810fa 0%, #0693e3 100%);
border-radius: 10px;
padding: 22px 26px;
color: #fff;
margin-bottom: 18px;
box-shadow: 0 6px 18px rgba(9,10,33,0.06);
}
.igny8-module-title h2 {
color: #fff;
font-size: 20px;
margin: 0;
font-weight: 700;
}
.igny8-module-title p {
margin: 6px 0 0 0;
color: rgba(255,255,255,0.9);
font-size: 14px;
}
/* Prevent top-grid cards from visually bleeding into the next panels */
.igny8-top-grid + .igny8-settings-card,
.igny8-top-grid + .automation-block {
margin-top: 32px;
}
/* Connection status single-row layout */
.igny8-connection-status-display {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
background: linear-gradient(135deg, #F9FAFB 0%, #F3F4F6 100%);
border: 1px solid #E5E7EB;
border-radius: 12px;
margin-bottom: 20px;
}
.igny8-connection-status-display .igny8-status-label {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #6B7280;
margin: 0;
font-weight: 600;
}
.igny8-connection-status-display .igny8-status-value {
font-size: 18px;
font-weight: 700;
color: #111827;
display: flex;
align-items: center;
gap: 10px;
}
.igny8-connection-status-display .igny8-status-value span {
margin: 0;
}
@media (max-width: 900px) {
.igny8-connection-status-display {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.igny8-module-title {
padding: 16px;
}
}
/* Top two-column layout for API Connection / Communication */
.igny8-top-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
align-items: start;
margin-bottom: 32px; /* ensure separation from subsequent sections */
}
@media (max-width: 900px) {
.igny8-top-grid {
grid-template-columns: 1fr;
}
}
/* Automation settings split into two columns */
.igny8-automation-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
align-items: start;
}
.igny8-top-grid > .igny8-settings-card {
margin: 0; /* grid handles spacing */
position: relative;
z-index: 1; /* prevent visual stacking/bleeding */
min-height: 160px; /* ensure cards don't collapse and overlap following content */
box-sizing: border-box;
}
.igny8-settings-card {
position: relative; /* ensure proper stacking context */
z-index: 0;
}
.automation-column-left,
.automation-column-right {
background: transparent;
}
@media (max-width: 900px) {
.igny8-automation-grid {
grid-template-columns: 1fr;
}
}
.igny8-settings-card {
background: #fff;
border: 1px solid #E5E7EB;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
padding: 24px;
margin: 24px 0;
border-radius: 8px;
}
.igny8-settings-card h2 {
margin-top: 0;
padding-bottom: 12px;
border-bottom: 2px solid var(--igny8-light-gray);
color: #111827;
font-size: 20px;
font-weight: 600;
}
/* ============================================
Toggle Switch
============================================ */
.igny8-toggle-wrapper {
display: flex;
align-items: center;
gap: 12px;
}
.igny8-toggle-input {
position: relative;
width: 48px;
height: 24px;
-webkit-appearance: none;
appearance: none;
background: var(--igny8-gray);
outline: none;
border-radius: 24px;
cursor: pointer;
transition: 0.3s;
}
.igny8-toggle-input:checked {
background: var(--igny8-success);
}
.igny8-toggle-input::before {
content: '';
position: absolute;
width: 20px;
height: 20px;
border-radius: 50%;
top: 2px;
left: 2px;
background: #fff;
transition: 0.3s;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.igny8-toggle-input:checked::before {
left: 26px;
}
/* ============================================
Sync Operations Grid
============================================ */
.igny8-sync-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
margin-top: 20px;
}
.igny8-sync-card {
background: #fff;
border: 2px solid #E5E7EB;
border-radius: 12px;
padding: 24px;
transition: all 0.3s ease;
}
.igny8-sync-card:hover {
border-color: var(--igny8-primary);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
transform: translateY(-2px);
}
.igny8-sync-card-highlight {
border-color: var(--igny8-primary);
background: linear-gradient(135deg, #EFF6FF 0%, #DBEAFE 100%);
}
.igny8-sync-icon {
width: 48px;
height: 48px;
background: var(--igny8-primary);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
color: white;
}
.igny8-sync-card h3 {
margin: 0 0 8px 0;
font-size: 16px;
font-weight: 600;
color: #111827;
}
.igny8-sync-description {
font-size: 14px;
color: #6B7280;
line-height: 1.5;
margin-bottom: 12px;
}
.igny8-sync-meta {
font-size: 12px;
color: #9CA3AF;
margin-bottom: 16px;
}
.igny8-sync-time {
display: inline-flex;
align-items: center;
gap: 4px;
}
.igny8-sync-button {
width: 100%;
height: 40px;
background: var(--igny8-primary) !important;
border-color: var(--igny8-primary) !important;
color: white !important;
font-weight: 500 !important;
border-radius: 8px !important;
transition: all 0.2s ease !important;
}
.igny8-sync-button:hover:not(:disabled) {
background: var(--igny8-primary-hover) !important;
border-color: var(--igny8-primary-hover) !important;
transform: translateY(-1px);
box-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.3);
}
.igny8-sync-button:disabled {
opacity: 0.5 !important;
cursor: not-allowed !important;
}
.button-loading {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
/* ============================================
Statistics Cards
============================================ */
.igny8-stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-top: 20px;
}
.igny8-stat-card {
background: linear-gradient(135deg, #ffffff 0%, #f9fafb 100%);
border: 1px solid #E5E7EB;
border-radius: 12px;
padding: 20px;
display: flex;
align-items: flex-start;
gap: 16px;
transition: all 0.3s ease;
}
.igny8-stat-card:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
transform: translateY(-2px);
}
.igny8-stat-icon {
width: 48px;
height: 48px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.igny8-stat-content {
flex: 1;
}
.igny8-stat-label {
font-size: 12px;
color: #6B7280;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 4px;
font-weight: 500;
}
.igny8-stat-value {
font-size: 28px;
font-weight: 700;
color: #111827;
line-height: 1.2;
margin-bottom: 4px;
}
.igny8-stat-meta {
font-size: 12px;
color: #9CA3AF;
}
/* ============================================
Semantic Summary
============================================ */
.igny8-semantic-summary {
margin-top: 24px;
padding: 20px;
background: linear-gradient(135deg, #F3E8FF 0%, #E9D5FF 100%);
border-radius: 12px;
border: 1px solid #D8B4FE;
}
.igny8-semantic-summary h3 {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
color: #6B21A8;
}
.igny8-semantic-stats {
display: flex;
gap: 32px;
flex-wrap: wrap;
}
.igny8-semantic-stat {
display: flex;
flex-direction: column;
gap: 4px;
}
.igny8-semantic-stat .value {
font-size: 24px;
font-weight: 700;
color: #7C3AED;
}
.igny8-semantic-stat .label {
font-size: 12px;
color: #8B5CF6;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* ============================================
Status Indicators
============================================ */
.igny8-status-connected {
color: var(--igny8-success);
font-weight: 600;
}
.igny8-status-disconnected {
color: var(--igny8-error);
font-weight: 600;
}
/* ============================================
API Connection Form
============================================ */
.igny8-api-connection-form {
background: linear-gradient(135deg, #ffffff 0%, #f9fafb 100%);
border: 2px solid #E5E7EB;
border-radius: 12px;
padding: 32px;
margin: 0 0 24px 0;
}
.igny8-api-form-group {
margin-bottom: 20px;
}
.igny8-api-form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #111827;
font-size: 14px;
}
.igny8-api-form-group input[type="text"],
.igny8-api-form-group input[type="password"] {
width: 100%;
padding: 12px 14px;
border: 1px solid #D1D5DB;
border-radius: 8px;
font-size: 14px;
transition: all 0.2s ease;
font-family: 'Courier New', monospace;
background-color: #fff;
}
.igny8-api-form-group input[type="text"]:focus,
.igny8-api-form-group input[type="password"]:focus {
outline: none;
border-color: var(--igny8-primary);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.igny8-api-form-group input[type="text"]:disabled {
background-color: #F3F4F6;
color: #9CA3AF;
cursor: not-allowed;
}
.igny8-api-form-description {
font-size: 13px;
color: #6B7280;
margin-top: 6px;
line-height: 1.5;
}
.igny8-api-form-description a {
color: var(--igny8-primary);
text-decoration: none;
font-weight: 500;
}
.igny8-api-form-description a:hover {
text-decoration: underline;
}
.igny8-connection-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-top: 24px;
}
.igny8-connection-actions .button {
border-radius: 8px;
padding: 10px 20px;
font-weight: 500;
border: none;
cursor: pointer;
transition: all 0.2s ease;
height: auto;
}
.igny8-connection-actions .button-primary {
background: var(--igny8-primary) !important;
color: white !important;
}
.igny8-connection-actions .button-primary:hover {
background: var(--igny8-primary-hover) !important;
transform: translateY(-1px);
box-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.3);
}
.igny8-connection-actions .button-secondary {
background: #E5E7EB !important;
color: #111827 !important;
}
.igny8-connection-actions .button-secondary:hover {
background: #D1D5DB !important;
}
.igny8-connection-status-display {
padding: 20px;
background: linear-gradient(135deg, #F9FAFB 0%, #F3F4F6 100%);
border: 1px solid #E5E7EB;
border-radius: 12px;
margin-bottom: 20px;
}
.igny8-connection-status-display .igny8-status-label {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #6B7280;
margin-bottom: 8px;
font-weight: 500;
}
.igny8-connection-status-display .igny8-status-value {
font-size: 20px;
font-weight: 700;
display: flex;
align-items: center;
gap: 8px;
}
.igny8-status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
}
.igny8-status-indicator.connected {
background-color: var(--igny8-success);
}
.igny8-status-indicator.disconnected {
background-color: var(--igny8-gray);
}
.igny8-api-key-display {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background-color: #F3F4F6;
border-radius: 8px;
word-break: break-all;
}
.igny8-api-key-mask {
font-family: 'Courier New', monospace;
color: #6B7280;
font-size: 14px;
}
/* ============================================
Diagnostics
============================================ */
.igny8-diagnostics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
margin-top: 20px;
}
.igny8-diagnostic-item {
padding: 16px;
background: linear-gradient(135deg, #F9FAFB 0%, #F3F4F6 100%);
border: 1px solid #E5E7EB;
border-radius: 8px;
transition: all 0.2s ease;
}
.igny8-diagnostic-item:hover {
border-color: var(--igny8-primary);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.igny8-diagnostic-label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #6B7280;
margin-bottom: 8px;
font-weight: 500;
}
.igny8-diagnostic-value {
font-size: 16px;
font-weight: 600;
color: #111827;
}
.igny8-diagnostic-item .description {
margin: 6px 0 0;
color: #9CA3AF;
font-size: 12px;
}
/* ============================================
Sync Status Messages
============================================ */
.igny8-sync-status {
margin-top: 20px;
padding: 16px;
border-radius: 8px;
display: none;
font-size: 14px;
}
.igny8-sync-status.igny8-sync-status-success {
background-color: #D1FAE5;
border: 1px solid #6EE7B7;
color: #065F46;
display: block;
}
.igny8-sync-status.igny8-sync-status-error {
background-color: #FEE2E2;
border: 1px solid #FCA5A5;
color: #991B1B;
display: block;
}
.igny8-sync-status.igny8-sync-status-loading {
background-color: #DBEAFE;
border: 1px solid #93C5FD;
color: #1E40AF;
display: block;
}
/* ============================================
Loading States
============================================ */
.igny8-loading {
opacity: 0.6;
pointer-events: none;
}
@keyframes igny8-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* ============================================
Messages & Notifications
============================================ */
.igny8-message {
padding: 16px;
margin: 15px 0;
border-left: 4px solid;
background: #fff;
border-radius: 4px;
}
.igny8-message.igny8-message-success {
border-color: var(--igny8-success);
background-color: #F0FDF4;
color: #065F46;
}
.igny8-message.igny8-message-error {
border-color: var(--igny8-error);
background-color: #FEF2F2;
color: #991B1B;
}
.igny8-message.igny8-message-info {
border-color: var(--igny8-primary);
background-color: #EFF6FF;
color: #1E40AF;
}
.igny8-message.igny8-message-warning {
border-color: var(--igny8-warning);
background-color: #FFFBEB;
color: #92400E;
}
/* ============================================
Responsive
============================================ */
@media (max-width: 782px) {
.igny8-sync-grid {
grid-template-columns: 1fr;
}
.igny8-stats-grid {
grid-template-columns: 1fr;
}
.igny8-diagnostics-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 600px) {
.igny8-settings-card {
padding: 16px;
}
.igny8-sync-card {
padding: 16px;
}
.igny8-stat-card {
flex-direction: column;
}
}

View File

@@ -0,0 +1,188 @@
/**
* 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('<span class="igny8-loading">Testing...</span>');
$.ajax({
url: igny8Admin.ajaxUrl,
type: 'POST',
data: {
action: 'igny8_test_connection',
nonce: igny8Admin.nonce
},
success: function(response) {
if (response.success) {
$result.html('<span class="igny8-success">✓ ' + (response.data.message || 'Connection successful') + '</span>');
} else {
var errorMsg = response.data.message || 'Connection failed';
var httpStatus = response.data.http_status || '';
var fullMsg = errorMsg;
if (httpStatus) {
fullMsg += ' (HTTP ' + httpStatus + ')';
}
$result.html('<span class="igny8-error">✗ ' + fullMsg + '</span>');
// Log full error to console for debugging
console.error('IGNY8 Connection Test Failed:', response.data);
}
},
error: function(xhr, status, error) {
$result.html('<span class="igny8-error">✗ Request failed: ' + error + '</span>');
console.error('IGNY8 AJAX Error:', xhr, status, error);
},
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('<span class="igny8-spinner"></span>' + 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);

View File

@@ -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('<p>' + response.data.message + '</p>')
.show();
// Reload page to show updated brief
setTimeout(function() {
location.reload();
}, 1000);
} else {
$message.addClass('notice notice-error inline')
.html('<p>' + (response.data.message || 'Failed to fetch brief') + '</p>')
.show();
$button.prop('disabled', false).text('Fetch Brief');
}
},
error: function() {
$message.addClass('notice notice-error inline')
.html('<p>Request failed</p>')
.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('<p>' + response.data.message + '</p>')
.show();
} else {
$message.addClass('notice notice-error inline')
.html('<p>' + (response.data.message || 'Failed to request refresh') + '</p>')
.show();
}
$button.prop('disabled', false).text('Request Refresh');
},
error: function() {
$message.addClass('notice notice-error inline')
.html('<p>Request failed</p>')
.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('<p>' + response.data.message + '</p>')
.show();
// Reload page to show updated status
setTimeout(function() {
location.reload();
}, 1000);
} else {
$message.addClass('notice notice-error inline')
.html('<p>' + (response.data.message || 'Failed to create job') + '</p>')
.show();
$button.prop('disabled', false).text('Request Optimization');
}
},
error: function() {
$message.addClass('notice notice-error inline')
.html('<p>Request failed</p>')
.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('<p>Status: <strong>' + response.data.status + '</strong></p>')
.show();
// Reload page to show updated status
setTimeout(function() {
location.reload();
}, 1000);
} else {
$message.addClass('notice notice-error inline')
.html('<p>' + (response.data.message || 'Failed to get status') + '</p>')
.show();
}
$button.prop('disabled', false).text('Check Status');
},
error: function() {
$message.addClass('notice notice-error inline')
.html('<p>Request failed</p>')
.show();
$button.prop('disabled', false).text('Check Status');
}
});
});
});
})(jQuery);

View File

@@ -0,0 +1,255 @@
<?php
/**
* Admin Columns and Row Actions
*
* Adds custom columns and actions to post/page/product list tables
*
* @package Igny8Bridge
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
/**
* Igny8AdminColumns Class
*/
class Igny8AdminColumns {
/**
* Constructor
*/
public function __construct() {
// Add columns for posts, pages, and products
add_filter('manage_posts_columns', array($this, 'add_columns'));
add_filter('manage_pages_columns', array($this, 'add_columns'));
// Add column content
add_action('manage_posts_custom_column', array($this, 'render_column_content'), 10, 2);
add_action('manage_pages_custom_column', array($this, 'render_column_content'), 10, 2);
// Make columns sortable
add_filter('manage_edit-post_sortable_columns', array($this, 'make_columns_sortable'));
add_filter('manage_edit-page_sortable_columns', array($this, 'make_columns_sortable'));
// Add row actions
add_filter('post_row_actions', array($this, 'add_row_actions'), 10, 2);
add_filter('page_row_actions', array($this, 'add_row_actions'), 10, 2);
// Handle WooCommerce products
if (class_exists('WooCommerce')) {
add_filter('manage_product_posts_columns', array($this, 'add_columns'));
add_action('manage_product_posts_custom_column', array($this, 'render_column_content'), 10, 2);
add_filter('manage_edit-product_sortable_columns', array($this, 'make_columns_sortable'));
add_filter('product_row_actions', array($this, 'add_row_actions'), 10, 2);
}
// Handle AJAX actions
add_action('wp_ajax_igny8_send_to_igny8', array($this, 'send_to_igny8'));
}
/**
* Add custom columns
*
* @param array $columns Existing columns
* @return array Modified columns
*/
public function add_columns($columns) {
// Removed custom taxonomy and attribute columns - posts now use native WordPress taxonomies (categories, tags, etc.)
return $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) {
// Removed custom column rendering - posts now use native WP taxonomies
return;
}
/**
* 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(
'<a href="%s" class="igny8-action-link" data-post-id="%d" data-action="update">%s</a>',
'#',
$post->ID,
__('Update in IGNY8', 'igny8-bridge')
);
} else {
// Not synced - show send action
$actions['igny8_send'] = sprintf(
'<a href="%s" class="igny8-action-link" data-post-id="%d" data-action="send">%s</a>',
'#',
$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'));

View File

@@ -0,0 +1,651 @@
<?php
/**
* Admin Interface Class
*
* Handles all admin functionality
*
* @package Igny8Bridge
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
/**
* Igny8Admin Class
*/
class Igny8Admin {
/**
* Single instance of the class
*
* @var Igny8Admin
*/
private static $instance = null;
/**
* Get single instance
*
* @return Igny8Admin
*/
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
add_action('admin_menu', array($this, 'add_menu_pages'));
add_action('admin_init', array($this, 'register_settings'));
add_action('admin_enqueue_scripts', array($this, 'enqueue_scripts'));
}
/**
* Add admin menu pages
*/
public function add_menu_pages() {
add_options_page(
__('IGNY8 API Settings', 'igny8-bridge'),
__('IGNY8 API', 'igny8-bridge'),
'manage_options',
'igny8-settings',
array($this, 'render_settings_page')
);
}
/**
* Register settings
*/
public function register_settings() {
// Email/password settings removed - using API key only
register_setting('igny8_settings', 'igny8_site_id');
register_setting('igny8_bridge_connection', 'igny8_connection_enabled', array(
'type' => 'boolean',
'sanitize_callback' => array($this, 'sanitize_boolean'),
'default' => 1
));
register_setting('igny8_bridge_connection', 'igny8_connection_enabled', array(
'type' => '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_enabled_taxonomies', array(
'type' => 'array',
'sanitize_callback' => array($this, 'sanitize_taxonomies'),
'default' => array('category', 'post_tag', 'product_cat', 'igny8_sectors', 'igny8_clusters')
));
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())
));
// Default post status for IGNY8 published content
register_setting('igny8_bridge_controls', 'igny8_default_post_status', array(
'type' => 'string',
'sanitize_callback' => array($this, 'sanitize_post_status'),
'default' => 'draft'
));
}
/**
* 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 (use wp_verify_nonce to avoid wp_die on failure)
if (isset($_POST['igny8_connect'])) {
if (empty($_POST['_wpnonce']) || !wp_verify_nonce($_POST['_wpnonce'], 'igny8_settings_nonce')) {
add_settings_error(
'igny8_settings',
'igny8_nonce',
__('Security check failed. Please refresh the page and try again.', 'igny8-bridge'),
'error'
);
} else {
$this->handle_connection();
}
}
// Handle revoke API key (use wp_verify_nonce)
if (isset($_POST['igny8_revoke_api_key'])) {
if (empty($_POST['_wpnonce']) || !wp_verify_nonce($_POST['_wpnonce'], 'igny8_revoke_api_key')) {
add_settings_error(
'igny8_settings',
'igny8_nonce_revoke',
__('Security check failed. Could not revoke API key.', 'igny8-bridge'),
'error'
);
} else {
self::revoke_api_key();
add_settings_error(
'igny8_settings',
'igny8_api_key_revoked',
__('API key revoked and removed from this site.', 'igny8-bridge'),
'updated'
);
}
}
// Webhook secret regeneration removed - using API key only
// Include settings template
include IGNY8_BRIDGE_PLUGIN_DIR . 'admin/settings.php';
}
/**
* Handle API connection - API key only
* Calls /v1/integration/integrations/test-connection/ endpoint
*/
private function handle_connection() {
$api_key = sanitize_text_field($_POST['igny8_api_key'] ?? '');
// API key is required
if (empty($api_key)) {
add_settings_error(
'igny8_settings',
'igny8_error',
__('API key is required to connect to IGNY8.', 'igny8-bridge'),
'error'
);
return;
}
// Extract site_id from API key format: igny8_site_{site_id}_{timestamp}_{random}
$site_id = null;
if (preg_match('/^igny8_site_(\d+)_/', $api_key, $matches)) {
$site_id = (int) $matches[1];
}
if (empty($site_id)) {
add_settings_error(
'igny8_settings',
'igny8_error',
__('Invalid API key format. Please copy the complete API key from IGNY8 app.', 'igny8-bridge'),
'error'
);
return;
}
// Get site URL
$site_url = get_site_url();
// Test connection using the correct integration test endpoint
// The API class will handle authentication for test-connection endpoint
// by using the API key from the request body
$api = new Igny8API();
$test_response = $api->post('/v1/integration/integrations/test-connection/', array(
'site_id' => $site_id,
'api_key' => $api_key,
'site_url' => $site_url
));
if (!$test_response['success']) {
$error_message = $test_response['error'] ?? 'Unknown error';
// Provide more user-friendly message for throttling errors
if (isset($test_response['http_status']) && $test_response['http_status'] === 429) {
$error_message = __('Rate limit exceeded. The plugin will automatically retry, but if this persists, please wait a moment and try again.', 'igny8-bridge');
}
add_settings_error(
'igny8_settings',
'igny8_error',
sprintf(
__('Failed to connect to IGNY8 API: %s', 'igny8-bridge'),
$error_message
),
'error'
);
return;
}
// Store API key securely
if (function_exists('igny8_store_secure_option')) {
igny8_store_secure_option('igny8_api_key', $api_key);
igny8_store_secure_option('igny8_access_token', $api_key);
} else {
update_option('igny8_api_key', $api_key);
update_option('igny8_access_token', $api_key);
}
// Store site ID
update_option('igny8_site_id', sanitize_text_field($site_id));
// Store integration_id from response if provided
$response_data = $test_response['data'] ?? array();
$integration_id = isset($response_data['integration_id']) ? intval($response_data['integration_id']) : null;
if ($integration_id) {
update_option('igny8_integration_id', $integration_id);
}
add_settings_error(
'igny8_settings',
'igny8_connected',
__('Successfully connected to IGNY8 API. Site registered.', 'igny8-bridge'),
'updated'
);
// Sync site structure to backend (post types, taxonomies, etc.)
// Pass integration_id if available to avoid querying
igny8_sync_site_structure_to_backend($integration_id);
}
/**
* Revoke stored API key (secure delete)
*
* Public so tests can call it directly.
*/
public static function revoke_api_key() {
if (function_exists('igny8_delete_secure_option')) {
igny8_delete_secure_option('igny8_api_key');
igny8_delete_secure_option('igny8_access_token');
igny8_delete_secure_option('igny8_refresh_token');
} else {
delete_option('igny8_api_key');
delete_option('igny8_access_token');
delete_option('igny8_refresh_token');
}
// Also clear token-issued timestamps
delete_option('igny8_token_refreshed_at');
delete_option('igny8_access_token_issued');
}
/**
* 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'));
}
// Get site ID
$site_id = get_option('igny8_site_id');
if (empty($site_id)) {
wp_send_json_error(array('message' => 'Site ID not configured. Connect to IGNY8 first.'));
}
// Test connection using the integration test endpoint
$api_key = function_exists('igny8_get_secure_option') ? igny8_get_secure_option('igny8_api_key') : get_option('igny8_api_key');
$test_response = $api->post('/v1/integration/integrations/test-connection/', array(
'site_id' => (int) $site_id,
'api_key' => $api_key,
'site_url' => get_site_url()
));
if ($test_response['success']) {
$checked_at = current_time('timestamp');
update_option('igny8_last_api_health_check', $checked_at);
wp_send_json_success(array(
'message' => __('Connection successful! IGNY8 API is responsive.', 'igny8-bridge'),
'checked_at' => $checked_at
));
return;
}
// Connection failed
$error_message = $test_response['error'] ?? 'Unknown error';
wp_send_json_error(array(
'message' => __('Connection failed: ', 'igny8-bridge') . $error_message,
'http_status' => $test_response['http_status'] ?? 0,
));
}
/**
* 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 taxonomies option
*
* @param mixed $value Raw value
* @return array
*/
public function sanitize_taxonomies($value) {
$supported = array_keys(igny8_get_supported_taxonomies());
if (!is_array($value)) {
return array('category', 'post_tag', 'product_cat', 'igny8_sectors', 'igny8_clusters');
}
$clean = array();
foreach ($value as $taxonomy) {
$taxonomy = sanitize_key($taxonomy);
if (in_array($taxonomy, $supported, true)) {
$clean[] = $taxonomy;
}
}
// Return defaults if nothing selected
return !empty($clean) ? $clean : array('category', 'post_tag');
}
/**
* 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;
}
/**
* Sanitize post status
*
* @param string $value Post status value
* @return string Sanitized post status
*/
public function sanitize_post_status($value) {
$allowed = array('draft', 'publish');
return in_array($value, $allowed, true) ? $value : 'draft';
}
}
// 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'));

View File

@@ -0,0 +1,413 @@
<?php
/**
* Post Meta Boxes
*
* Handles custom meta boxes for IGNY8-created posts.
* Displays keywords, SEO fields, and sync data in WordPress post editor.
*
* @package IGNY8_Bridge
* @since 1.1.0
*/
if (!defined('ABSPATH')) {
exit;
}
class IGNY8_Post_Meta_Boxes {
/**
* Initialize meta boxes
*/
public function __construct() {
add_action('add_meta_boxes', array($this, 'add_meta_boxes'));
add_action('save_post', array($this, 'save_meta_boxes'), 10, 2);
}
/**
* Register meta boxes
*/
public function add_meta_boxes() {
// Only add meta boxes for posts that have IGNY8 content ID
$screen = get_current_screen();
if ($screen && $screen->post_type === 'post') {
// SEO meta box (includes keywords now)
add_meta_box(
'igny8_seo',
__('IGNY8 SEO', 'igny8-bridge'),
array($this, 'render_seo_meta_box'),
'post',
'normal',
'high'
);
// Images meta box
add_meta_box(
'igny8_images',
__('IGNY8 Images', 'igny8-bridge'),
array($this, 'render_images_meta_box'),
'post',
'side',
'default'
);
// Sync data meta box (read-only info)
add_meta_box(
'igny8_sync_data',
__('IGNY8 Sync Data', 'igny8-bridge'),
array($this, 'render_sync_data_meta_box'),
'post',
'side',
'default'
);
}
}
/**
* Render Images meta box
*/
public function render_images_meta_box($post) {
$featured_image_id = get_post_thumbnail_id($post->ID);
$gallery_images = get_post_meta($post->ID, '_igny8_gallery_images', true);
$in_article_images = get_post_meta($post->ID, '_igny8_imported_images', true);
?>
<div class="igny8-images-info">
<?php if ($featured_image_id): ?>
<p>
<strong><?php _e('Featured Image:', 'igny8-bridge'); ?></strong><br>
<?php echo wp_get_attachment_image($featured_image_id, 'thumbnail'); ?>
</p>
<?php else: ?>
<p class="description"><?php _e('No featured image set', 'igny8-bridge'); ?></p>
<?php endif; ?>
<?php if (!empty($gallery_images) && is_array($gallery_images)): ?>
<p>
<strong><?php _e('Gallery Images:', 'igny8-bridge'); ?> (<?php echo count($gallery_images); ?>)</strong><br>
<div style="display: flex; flex-wrap: wrap; gap: 5px; margin-top: 5px;">
<?php foreach (array_slice($gallery_images, 0, 4) as $img_id): ?>
<?php echo wp_get_attachment_image($img_id, 'thumbnail', false, array('style' => 'max-width: 60px; height: auto;')); ?>
<?php endforeach; ?>
<?php if (count($gallery_images) > 4): ?>
<span style="align-self: center; color: #666;">+<?php echo count($gallery_images) - 4; ?> more</span>
<?php endif; ?>
</div>
</p>
<?php endif; ?>
<?php if (!empty($in_article_images) && is_array($in_article_images)): ?>
<p>
<strong><?php _e('In-Article Images:', 'igny8-bridge'); ?></strong><br>
<span class="description"><?php echo count($in_article_images); ?> images embedded in content</span>
</p>
<?php endif; ?>
<?php if (!$featured_image_id && empty($gallery_images) && empty($in_article_images)): ?>
<p class="description"><?php _e('No images published yet', 'igny8-bridge'); ?></p>
<?php endif; ?>
</div>
<style>
.igny8-images-info img {
border: 1px solid #ddd;
border-radius: 3px;
padding: 2px;
}
.igny8-images-info p {
margin: 10px 0;
}
</style>
<?php
}
/**
* Render SEO meta box
*/
public function render_seo_meta_box($post) {
wp_nonce_field('igny8_seo_nonce', 'igny8_seo_nonce');
$cluster_name = get_post_meta($post->ID, '_igny8_cluster_name', true);
$primary_keyword = get_post_meta($post->ID, '_igny8_primary_keyword', true);
$secondary_keywords = get_post_meta($post->ID, '_igny8_secondary_keywords', true);
$meta_title = get_post_meta($post->ID, '_igny8_meta_title', true);
$meta_description = get_post_meta($post->ID, '_igny8_meta_description', true);
// Convert comma-separated string to array for display
$secondary_keywords_array = !empty($secondary_keywords) ? explode(',', $secondary_keywords) : array();
?>
<div class="igny8-seo-fields">
<?php if ($cluster_name): ?>
<p style="margin-bottom: 15px;">
<strong style="font-size: 14px;"><?php echo esc_html($cluster_name); ?></strong>
</p>
<?php endif; ?>
<p>
<label for="igny8_primary_keyword"><strong><?php _e('Primary Keyword:', 'igny8-bridge'); ?></strong></label><br>
<input type="text" id="igny8_primary_keyword" name="igny8_primary_keyword"
value="<?php echo esc_attr($primary_keyword); ?>"
class="widefat"
placeholder="<?php _e('Enter primary keyword', 'igny8-bridge'); ?>">
</p>
<p>
<label for="igny8_secondary_keywords"><strong><?php _e('Secondary Keywords:', 'igny8-bridge'); ?></strong></label><br>
<textarea id="igny8_secondary_keywords" name="igny8_secondary_keywords"
rows="3" class="widefat"
placeholder="<?php _e('Enter secondary keywords, one per line', 'igny8-bridge'); ?>"><?php
echo esc_textarea(implode("\n", $secondary_keywords_array));
?></textarea>
<span class="description"><?php _e('Enter one keyword per line', 'igny8-bridge'); ?></span>
</p>
<p>
<label for="igny8_meta_title"><strong><?php _e('SEO Title:', 'igny8-bridge'); ?></strong></label><br>
<input type="text" id="igny8_meta_title" name="igny8_meta_title"
value="<?php echo esc_attr($meta_title); ?>"
class="widefat"
placeholder="<?php _e('Enter SEO title', 'igny8-bridge'); ?>"
maxlength="60">
<span class="description char-count">
<?php
$title_length = mb_strlen($meta_title);
printf(__('%d characters (recommended: 50-60)', 'igny8-bridge'), $title_length);
?>
</span>
</p>
<p>
<label for="igny8_meta_description"><strong><?php _e('Meta Description:', 'igny8-bridge'); ?></strong></label><br>
<textarea id="igny8_meta_description" name="igny8_meta_description"
rows="3" class="widefat"
placeholder="<?php _e('Enter meta description', 'igny8-bridge'); ?>"
maxlength="160"><?php echo esc_textarea($meta_description); ?></textarea>
<span class="description char-count">
<?php
$desc_length = mb_strlen($meta_description);
printf(__('%d characters (recommended: 150-160)', 'igny8-bridge'), $desc_length);
?>
</span>
</p>
<p class="description">
<strong><?php _e('Note:', 'igny8-bridge'); ?></strong>
<?php _e('These SEO fields are synchronized with Yoast SEO, Rank Math, and All in One SEO plugins.', 'igny8-bridge'); ?>
</p>
</div>
<style>
/* Max width when shown below editor */
#igny8_seo.postbox {
max-width: 800px;
}
/* Full width when in sidebar */
.inner-sidebar #igny8_seo.postbox {
max-width: none;
}
.igny8-seo-fields input[type="text"],
.igny8-seo-fields textarea {
margin-top: 5px;
}
.igny8-seo-fields .description {
display: block;
margin-top: 5px;
font-style: italic;
color: #666;
}
.igny8-seo-fields .char-count {
font-size: 12px;
}
</style>
<script>
jQuery(document).ready(function($) {
// Character counter for SEO title
$('#igny8_meta_title').on('input', function() {
var length = $(this).val().length;
var color = length > 60 ? 'red' : (length < 50 ? 'orange' : 'green');
$(this).next('.char-count').text(length + ' characters (recommended: 50-60)').css('color', color);
});
// Character counter for meta description
$('#igny8_meta_description').on('input', function() {
var length = $(this).val().length;
var color = length > 160 ? 'red' : (length < 150 ? 'orange' : 'green');
$(this).next('.char-count').text(length + ' characters (recommended: 150-160)').css('color', color);
});
});
</script>
<?php
}
/**
* Render Sync Data meta box (read-only)
*/
public function render_sync_data_meta_box($post) {
$content_id = get_post_meta($post->ID, '_igny8_content_id', true);
$task_id = get_post_meta($post->ID, '_igny8_task_id', true);
$last_sync = get_post_meta($post->ID, '_igny8_last_sync', true);
$cluster_id = get_post_meta($post->ID, '_igny8_cluster_id', true);
$sector_id = get_post_meta($post->ID, '_igny8_sector_id', true);
?>
<div class="igny8-sync-data">
<?php if ($content_id): ?>
<p>
<strong><?php _e('IGNY8 Content ID:', 'igny8-bridge'); ?></strong><br>
<code><?php echo esc_html($content_id); ?></code>
</p>
<?php endif; ?>
<?php if ($task_id): ?>
<p>
<strong><?php _e('IGNY8 Task ID:', 'igny8-bridge'); ?></strong><br>
<code><?php echo esc_html($task_id); ?></code>
</p>
<?php endif; ?>
<?php if ($cluster_id): ?>
<p>
<strong><?php _e('Cluster ID:', 'igny8-bridge'); ?></strong><br>
<code><?php echo esc_html($cluster_id); ?></code>
</p>
<?php endif; ?>
<?php if ($sector_id): ?>
<p>
<strong><?php _e('Sector ID:', 'igny8-bridge'); ?></strong><br>
<code><?php echo esc_html($sector_id); ?></code>
</p>
<?php endif; ?>
<?php if ($last_sync): ?>
<p>
<strong><?php _e('Last Synced:', 'igny8-bridge'); ?></strong><br>
<?php echo esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($last_sync))); ?>
</p>
<?php endif; ?>
<?php if (!$content_id && !$task_id): ?>
<p class="description">
<?php _e('This post was not created by IGNY8.', 'igny8-bridge'); ?>
</p>
<?php endif; ?>
</div>
<style>
.igny8-sync-data p {
margin: 10px 0;
}
.igny8-sync-data code {
display: inline-block;
background: #f0f0f0;
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
}
</style>
<?php
}
/**
* Save meta boxes
*/
public function save_meta_boxes($post_id, $post) {
// Check if this is an autosave
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
// Check if this is a revision
if (wp_is_post_revision($post_id)) {
return;
}
// Check post type
if ($post->post_type !== 'post') {
return;
}
// Check user permissions
if (!current_user_can('edit_post', $post_id)) {
return;
}
// Save SEO fields (includes keywords now)
if (isset($_POST['igny8_seo_nonce']) && wp_verify_nonce($_POST['igny8_seo_nonce'], 'igny8_seo_nonce')) {
// Save primary keyword
if (isset($_POST['igny8_primary_keyword'])) {
$primary_keyword = sanitize_text_field($_POST['igny8_primary_keyword']);
update_post_meta($post_id, '_igny8_primary_keyword', $primary_keyword);
}
// Save secondary keywords
if (isset($_POST['igny8_secondary_keywords'])) {
$secondary_keywords_raw = sanitize_textarea_field($_POST['igny8_secondary_keywords']);
// Convert newlines to commas for storage
$secondary_keywords_array = array_filter(array_map('trim', explode("\n", $secondary_keywords_raw)));
$secondary_keywords = implode(',', $secondary_keywords_array);
update_post_meta($post_id, '_igny8_secondary_keywords', $secondary_keywords);
}
// Save meta title
if (isset($_POST['igny8_meta_title'])) {
$meta_title = sanitize_text_field($_POST['igny8_meta_title']);
update_post_meta($post_id, '_igny8_meta_title', $meta_title);
// Also update for SEO plugins
$this->sync_seo_title($post_id, $meta_title);
}
// Save meta description
if (isset($_POST['igny8_meta_description'])) {
$meta_description = sanitize_textarea_field($_POST['igny8_meta_description']);
update_post_meta($post_id, '_igny8_meta_description', $meta_description);
// Also update for SEO plugins
$this->sync_seo_description($post_id, $meta_description);
}
}
}
/**
* Sync SEO title to popular SEO plugins
*/
private function sync_seo_title($post_id, $title) {
if (empty($title)) {
return;
}
// Yoast SEO
update_post_meta($post_id, '_yoast_wpseo_title', $title);
// Rank Math
update_post_meta($post_id, 'rank_math_title', $title);
// All in One SEO
update_post_meta($post_id, '_aioseo_title', $title);
}
/**
* Sync SEO description to popular SEO plugins
*/
private function sync_seo_description($post_id, $description) {
if (empty($description)) {
return;
}
// Yoast SEO
update_post_meta($post_id, '_yoast_wpseo_metadesc', $description);
// Rank Math
update_post_meta($post_id, 'rank_math_description', $description);
// All in One SEO
update_post_meta($post_id, '_aioseo_description', $description);
}
}
// Initialize
new IGNY8_Post_Meta_Boxes();

View File

@@ -0,0 +1,469 @@
<?php
/**
* Post Meta Boxes
*
* Adds meta boxes to post editor for IGNY8 features
*
* @package Igny8Bridge
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
/**
* Igny8PostMetaBoxes Class
*/
class Igny8PostMetaBoxes {
/**
* Constructor
*/
public function __construct() {
add_action('add_meta_boxes', array($this, 'add_meta_boxes'));
add_action('admin_enqueue_scripts', array($this, 'enqueue_scripts'));
// AJAX handlers
add_action('wp_ajax_igny8_fetch_planner_brief', array($this, 'fetch_planner_brief'));
add_action('wp_ajax_igny8_refresh_planner_task', array($this, 'refresh_planner_task'));
add_action('wp_ajax_igny8_create_optimizer_job', array($this, 'create_optimizer_job'));
add_action('wp_ajax_igny8_get_optimizer_status', array($this, 'get_optimizer_status'));
}
/**
* Add meta boxes to post editor
*/
public function add_meta_boxes() {
$post_types = array('post', 'page', 'product');
foreach ($post_types as $post_type) {
add_meta_box(
'igny8-planner-brief',
__('IGNY8 Planner Brief', 'igny8-bridge'),
array($this, 'render_planner_brief_box'),
$post_type,
'side',
'default'
);
add_meta_box(
'igny8-optimizer',
__('IGNY8 Optimizer', 'igny8-bridge'),
array($this, 'render_optimizer_box'),
$post_type,
'side',
'default'
);
}
}
/**
* Enqueue scripts for post editor
*/
public function enqueue_scripts($hook) {
if (!in_array($hook, array('post.php', 'post-new.php'), true)) {
return;
}
wp_enqueue_script(
'igny8-post-editor',
IGNY8_BRIDGE_PLUGIN_URL . 'admin/assets/js/post-editor.js',
array('jquery'),
IGNY8_BRIDGE_VERSION,
true
);
wp_localize_script('igny8-post-editor', 'igny8PostEditor', array(
'ajaxUrl' => 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 '<p class="description">';
_e('This post is not linked to an IGNY8 task or cluster.', 'igny8-bridge');
echo '</p>';
return;
}
wp_nonce_field('igny8_post_editor_nonce', 'igny8_post_editor_nonce');
?>
<div id="igny8-planner-brief-content">
<?php if ($brief) : ?>
<div class="igny8-brief-display">
<?php if (is_array($brief)) : ?>
<?php if (!empty($brief['title'])) : ?>
<h4><?php echo esc_html($brief['title']); ?></h4>
<?php endif; ?>
<?php if (!empty($brief['content'])) : ?>
<div class="igny8-brief-content">
<?php echo wp_kses_post(wpautop($brief['content'])); ?>
</div>
<?php endif; ?>
<?php if (!empty($brief['outline'])) : ?>
<div class="igny8-brief-outline">
<strong><?php _e('Outline:', 'igny8-bridge'); ?></strong>
<?php if (is_array($brief['outline'])) : ?>
<ul>
<?php foreach ($brief['outline'] as $item) : ?>
<li><?php echo esc_html($item); ?></li>
<?php endforeach; ?>
</ul>
<?php else : ?>
<p><?php echo esc_html($brief['outline']); ?></p>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if (!empty($brief['keywords'])) : ?>
<div class="igny8-brief-keywords">
<strong><?php _e('Keywords:', 'igny8-bridge'); ?></strong>
<?php
$keywords = is_array($brief['keywords']) ? $brief['keywords'] : explode(',', $brief['keywords']);
echo '<span class="igny8-keyword-tags">';
foreach ($keywords as $keyword) {
echo '<span class="igny8-keyword-tag">' . esc_html(trim($keyword)) . '</span>';
}
echo '</span>';
?>
</div>
<?php endif; ?>
<?php if (!empty($brief['tone'])) : ?>
<div class="igny8-brief-tone">
<strong><?php _e('Tone:', 'igny8-bridge'); ?></strong>
<?php echo esc_html($brief['tone']); ?>
</div>
<?php endif; ?>
<?php else : ?>
<p><?php echo esc_html($brief); ?></p>
<?php endif; ?>
<?php if ($brief_cached_at) : ?>
<p class="description">
<?php
printf(
__('Cached: %s', 'igny8-bridge'),
date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($brief_cached_at))
);
?>
</p>
<?php endif; ?>
</div>
<?php else : ?>
<p class="description">
<?php _e('No brief cached. Click "Fetch Brief" to load from IGNY8.', 'igny8-bridge'); ?>
</p>
<?php endif; ?>
</div>
<p>
<button type="button"
id="igny8-fetch-brief"
class="button button-secondary"
data-post-id="<?php echo esc_attr($post->ID); ?>"
data-task-id="<?php echo esc_attr($task_id); ?>">
<?php _e('Fetch Brief', 'igny8-bridge'); ?>
</button>
<?php if ($task_id) : ?>
<button type="button"
id="igny8-refresh-task"
class="button button-secondary"
data-post-id="<?php echo esc_attr($post->ID); ?>"
data-task-id="<?php echo esc_attr($task_id); ?>">
<?php _e('Request Refresh', 'igny8-bridge'); ?>
</button>
<?php endif; ?>
</p>
<div id="igny8-planner-brief-message" class="igny8-message" style="display: none;"></div>
<?php
}
/**
* Render Optimizer meta box
*/
public function render_optimizer_box($post) {
$task_id = get_post_meta($post->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 '<p class="description">';
_e('This post is not linked to an IGNY8 task.', 'igny8-bridge');
echo '</p>';
return;
}
wp_nonce_field('igny8_post_editor_nonce', 'igny8_post_editor_nonce');
?>
<div id="igny8-optimizer-content">
<?php if ($optimizer_job_id) : ?>
<div class="igny8-optimizer-status">
<p>
<strong><?php _e('Job ID:', 'igny8-bridge'); ?></strong>
<?php echo esc_html($optimizer_job_id); ?>
</p>
<?php if ($optimizer_status) : ?>
<p>
<strong><?php _e('Status:', 'igny8-bridge'); ?></strong>
<span class="igny8-status-badge igny8-status-<?php echo esc_attr(strtolower($optimizer_status)); ?>">
<?php echo esc_html($optimizer_status); ?>
</span>
</p>
<?php endif; ?>
<p>
<button type="button"
id="igny8-check-optimizer-status"
class="button button-secondary"
data-post-id="<?php echo esc_attr($post->ID); ?>"
data-job-id="<?php echo esc_attr($optimizer_job_id); ?>">
<?php _e('Check Status', 'igny8-bridge'); ?>
</button>
</p>
</div>
<?php else : ?>
<p class="description">
<?php _e('No optimizer job created yet.', 'igny8-bridge'); ?>
</p>
<?php endif; ?>
</div>
<p>
<button type="button"
id="igny8-create-optimizer-job"
class="button button-primary"
data-post-id="<?php echo esc_attr($post->ID); ?>"
data-task-id="<?php echo esc_attr($task_id); ?>">
<?php _e('Request Optimization', 'igny8-bridge'); ?>
</button>
</p>
<div id="igny8-optimizer-message" class="igny8-message" style="display: none;"></div>
<?php
}
/**
* Fetch Planner brief (AJAX handler)
*/
public static function fetch_planner_brief() {
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'));
}
// 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();

View File

@@ -0,0 +1,716 @@
<?php
/**
* Settings Page Template
*
* @package Igny8Bridge
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
// Get current settings
$email = get_option('igny8_email', '');
$site_id = get_option('igny8_site_id', '');
$integration_id = get_option('igny8_integration_id', '');
$access_token = function_exists('igny8_get_secure_option') ? igny8_get_secure_option('igny8_access_token') : get_option('igny8_access_token');
$last_structure_sync = get_option('igny8_last_structure_sync', 0);
// Connection is complete only if: API key exists, integration_id exists, and structure has been synced
$is_connected = !empty($access_token) && !empty($integration_id) && !empty($last_structure_sync);
$api_key = function_exists('igny8_get_secure_option') ? igny8_get_secure_option('igny8_api_key') : get_option('igny8_api_key');
$date_format = get_option('date_format');
$time_format = get_option('time_format');
$now = current_time('timestamp');
$token_issued = intval(get_option('igny8_access_token_issued', 0));
$token_age_text = $token_issued ? sprintf(__('%s ago', 'igny8-bridge'), human_time_diff($token_issued, $now)) : __('Not generated yet', 'igny8-bridge');
$token_issued_formatted = $token_issued ? date_i18n($date_format . ' ' . $time_format, $token_issued) : __('—', 'igny8-bridge');
$last_health_check = intval(get_option('igny8_last_api_health_check', 0));
$last_health_check_formatted = $last_health_check ? date_i18n($date_format . ' ' . $time_format, $last_health_check) : __('Never', 'igny8-bridge');
$last_site_sync = intval(get_option('igny8_last_site_sync', 0));
$last_site_sync_formatted = $last_site_sync ? date_i18n($date_format . ' ' . $time_format, $last_site_sync) : __('Never', 'igny8-bridge');
$last_taxonomy_sync = intval(get_option('igny8_last_taxonomy_sync', 0));
$last_taxonomy_sync_formatted = $last_taxonomy_sync ? date_i18n($date_format . ' ' . $time_format, $last_taxonomy_sync) : __('Never', 'igny8-bridge');
$last_keyword_sync = intval(get_option('igny8_last_keyword_sync', 0));
$last_keyword_sync_formatted = $last_keyword_sync ? date_i18n($date_format . ' ' . $time_format, $last_keyword_sync) : __('Never', 'igny8-bridge');
$last_writer_sync = intval(get_option('igny8_last_writer_sync', 0));
$last_writer_sync_formatted = $last_writer_sync ? date_i18n($date_format . ' ' . $time_format, $last_writer_sync) : __('Never', 'igny8-bridge');
$last_full_site_scan = intval(get_option('igny8_last_full_site_scan', 0));
$last_full_site_scan_formatted = $last_full_site_scan ? date_i18n($date_format . ' ' . $time_format, $last_full_site_scan) : __('Never', 'igny8-bridge');
$last_semantic_map = intval(get_option('igny8_last_semantic_map', 0));
$last_semantic_map_formatted = $last_semantic_map ? date_i18n($date_format . ' ' . $time_format, $last_semantic_map) : __('Never', 'igny8-bridge');
$semantic_summary = get_option('igny8_last_semantic_map_summary', array());
$next_site_sync = wp_next_scheduled('igny8_sync_site_data');
$next_site_sync_formatted = $next_site_sync ? date_i18n($date_format . ' ' . $time_format, $next_site_sync) : __('Not scheduled', 'igny8-bridge');
$available_post_types = igny8_get_supported_post_types();
$enabled_post_types = igny8_get_enabled_post_types();
$available_taxonomies = igny8_get_supported_taxonomies();
$enabled_taxonomies = igny8_get_enabled_taxonomies();
$control_mode = igny8_get_control_mode();
$woocommerce_enabled = (int) get_option('igny8_enable_woocommerce', class_exists('WooCommerce') ? 1 : 0);
$woocommerce_detected = class_exists('WooCommerce');
$available_modules = igny8_get_available_modules();
$enabled_modules = igny8_get_enabled_modules();
$connection_enabled = igny8_is_connection_enabled();
$connection_state = igny8_get_connection_state();
$link_queue = get_option('igny8_link_queue', array());
$pending_links = array_filter($link_queue, function($item) {
return $item['status'] === 'pending';
});
$webhook_logs = igny8_get_webhook_logs(array('limit' => 10));
$default_post_status = get_option('igny8_default_post_status', 'draft');
?>
<div class="wrap">
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
<?php settings_errors('igny8_settings'); ?>
<div class="igny8-settings-container">
<div class="igny8-module-title igny8-page-header">
<h2>Igny8 Wordpress Bridge</h2>
</div>
<div class="igny8-top-grid">
<div class="igny8-settings-card">
<h2>
<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="vertical-align: middle; margin-right: 8px; display: inline;">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.658 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/>
</svg>
<?php _e('API Connection', 'igny8-bridge'); ?>
</h2>
<?php if (!$is_connected) : ?>
<div class="igny8-connection-status-display">
<div class="igny8-status-label"><?php _e('Status', 'igny8-bridge'); ?></div>
<div class="igny8-status-value">
<span class="igny8-status-indicator disconnected"></span>
<span style="color: #6B7280;"><?php _e('Not Connected', 'igny8-bridge'); ?></span>
</div>
</div>
<form method="post" action="">
<?php wp_nonce_field('igny8_settings_nonce'); ?>
<div class="igny8-api-form-group">
<label for="igny8_api_key">
<?php _e('API Key', 'igny8-bridge'); ?>
<span style="color: #EF4444;">*</span>
</label>
<input
type="text"
id="igny8_api_key"
name="igny8_api_key"
value=""
placeholder="<?php _e('igny8_site_5_1764575388582_u671q2e2mv', 'igny8-bridge'); ?>"
required
/>
<p class="igny8-api-form-description">
<?php printf(
__('Get your API key from the <a href="%s" target="_blank">IGNY8 app integrations page</a>. The API key format includes your site ID.', 'igny8-bridge'),
'https://app.igny8.com'
); ?>
</p>
</div>
<div class="igny8-connection-actions">
<?php submit_button(__('Connect to IGNY8', 'igny8-bridge'), 'primary', 'igny8_connect'); ?>
</div>
</form>
<?php else : ?>
<div class="igny8-connection-status-display">
<div class="igny8-status-label"><?php _e('Status', 'igny8-bridge'); ?></div>
<div class="igny8-status-value">
<span class="igny8-status-indicator connected"></span>
<span style="color: var(--igny8-success);"><?php _e('Connected', 'igny8-bridge'); ?></span>
</div>
</div>
<div class="igny8-api-form-group">
<label><?php _e('Active API Key', 'igny8-bridge'); ?></label>
<div class="igny8-api-key-display">
<span class="igny8-api-key-mask"><?php echo esc_html(substr($api_key, 0, 7) . '••••••••••••' . substr($api_key, -7)); ?></span>
<span style="color: #10B981; font-weight: 500;">✓ <?php _e('Verified', 'igny8-bridge'); ?></span>
</div>
</div>
<div class="igny8-api-form-group">
<label><?php _e('Site ID', 'igny8-bridge'); ?></label>
<div style="padding: 10px 12px; background-color: #F9FAFB; border: 1px solid #E5E7EB; border-radius: 4px;">
<strong><?php echo esc_html($site_id ?: __('Not set', 'igny8-bridge')); ?></strong>
</div>
</div>
<div class="igny8-connection-actions" style="margin-top: 20px;">
<form method="post" action="">
<?php wp_nonce_field('igny8_revoke_api_key'); ?>
<button type="submit" name="igny8_revoke_api_key" class="button button-secondary" onclick="return confirm('<?php _e('Are you sure you want to disconnect? This will stop all syncing with IGNY8.', 'igny8-bridge'); ?>');">
<?php _e('Disconnect', 'igny8-bridge'); ?>
</button>
</form>
</div>
<?php endif; ?>
</div>
<?php if ($is_connected) : ?>
<div class="igny8-settings-card">
<h2>
<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="vertical-align: middle; margin-right: 8px; display: inline;">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<?php _e('Communication Settings', 'igny8-bridge'); ?>
</h2>
<form method="post" action="options.php">
<?php settings_fields('igny8_bridge_connection'); ?>
<table class="form-table">
<tr>
<th scope="row">
<label for="igny8_connection_enabled"><?php _e('Enable Communication', 'igny8-bridge'); ?></label>
</th>
<td>
<label class="igny8-toggle-wrapper">
<input
type="checkbox"
id="igny8_connection_enabled"
name="igny8_connection_enabled"
value="1"
<?php checked($connection_enabled, 1); ?>
class="igny8-toggle-input"
/>
<span class="igny8-toggle-slider"></span>
<span class="igny8-toggle-label">
<?php if ($connection_enabled) : ?>
<strong style="color: #10B981;"><?php _e('Connected', 'igny8-bridge'); ?></strong>
<?php else : ?>
<strong style="color: #6B7280;"><?php _e('Disabled', 'igny8-bridge'); ?></strong>
<?php endif; ?>
</span>
</label>
<p class="description">
<?php _e('When enabled, the plugin will sync content and data with IGNY8. Disabling pauses all sync operations but keeps your API key stored.', 'igny8-bridge'); ?>
</p>
</td>
</tr>
</table>
<?php submit_button(__('Save Settings', 'igny8-bridge')); ?>
</form>
<div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #E5E7EB;">
<button type="button" id="igny8-test-connection" class="button" <?php disabled(!$connection_enabled); ?>>
<?php _e('Test Connection', 'igny8-bridge'); ?>
</button>
<span id="igny8-test-result" class="igny8-test-result"></span>
</div>
<?php if (defined('WP_DEBUG') && WP_DEBUG) : ?>
<p class="description" style="color: #0073aa; margin-top: 10px;">
<strong><?php _e('Debug Mode Active', 'igny8-bridge'); ?>:</strong>
<?php _e('Check wp-content/debug.log for detailed API request/response logs.', 'igny8-bridge'); ?>
</p>
<?php endif; ?>
</div>
</div>
<?php else : ?>
<div class="igny8-settings-card">
<h2><?php _e('Connection Status', 'igny8-bridge'); ?></h2>
<p>
<span class="igny8-status-disconnected">
<?php _e('Not Connected', 'igny8-bridge'); ?>
</span>
</p>
<p class="description">
<?php _e('Enter your IGNY8 credentials above and click "Connect to IGNY8" to establish a connection.', 'igny8-bridge'); ?>
</p>
</div>
<?php endif; ?>
<div class="igny8-settings-card">
<h2>
<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="vertical-align: middle; margin-right: 8px; display: inline;">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"/>
</svg>
<?php _e('Automation Settings', 'igny8-bridge'); ?>
</h2>
<form method="post" action="options.php">
<?php settings_fields('igny8_bridge_controls'); ?>
<div class="igny8-automation-grid">
<div class="automation-column-left">
<div class="automation-block">
<h3><?php _e('Post Types to Sync', 'igny8-bridge'); ?></h3>
<?php foreach ($available_post_types as $slug => $label) : ?>
<label>
<input
type="checkbox"
name="igny8_enabled_post_types[]"
value="<?php echo esc_attr($slug); ?>"
<?php checked(in_array($slug, $enabled_post_types, true)); ?>
/>
<?php echo esc_html($label); ?>
</label>
<br />
<?php endforeach; ?>
<p class="description">
<?php _e('Select the content types IGNY8 should manage automatically.', 'igny8-bridge'); ?>
</p>
</div>
<div class="automation-block">
<h3><?php _e('IGNY8 Modules', 'igny8-bridge'); ?></h3>
<?php foreach ($available_modules as $module_key => $module_label) : ?>
<label>
<input
type="checkbox"
name="igny8_enabled_modules[]"
value="<?php echo esc_attr($module_key); ?>"
<?php checked(in_array($module_key, $enabled_modules, true)); ?>
/>
<?php echo esc_html($module_label); ?>
</label>
<br />
<?php endforeach; ?>
<p class="description">
<?php _e('Disable modules temporarily if a feature is not ready in the SaaS app.', 'igny8-bridge'); ?>
</p>
</div>
<div class="automation-block">
<h3><?php _e('Control Mode', 'igny8-bridge'); ?></h3>
<label>
<input
type="radio"
name="igny8_control_mode"
value="mirror"
<?php checked($control_mode, 'mirror'); ?>
/>
<strong><?php _e('Mirror', 'igny8-bridge'); ?></strong>
<span class="description"><?php _e('IGNY8 is the source of truth; WordPress reflects changes only.', 'igny8-bridge'); ?></span>
</label>
<br />
<label>
<input
type="radio"
name="igny8_control_mode"
value="hybrid"
<?php checked($control_mode, 'hybrid'); ?>
/>
<strong><?php _e('Hybrid', 'igny8-bridge'); ?></strong>
<span class="description"><?php _e('Allow editors to update content in WordPress and sync back to IGNY8.', 'igny8-bridge'); ?></span>
</label>
</div>
<div class="automation-block">
<h3><?php _e('Default Post Status for IGNY8 Content', 'igny8-bridge'); ?></h3>
<label>
<input
type="radio"
name="igny8_default_post_status"
value="draft"
<?php checked($default_post_status, 'draft'); ?>
/>
<strong><?php _e('Draft', 'igny8-bridge'); ?></strong>
<span class="description"><?php _e('Save content as draft for review before publishing.', 'igny8-bridge'); ?></span>
</label>
<br />
<label>
<input
type="radio"
name="igny8_default_post_status"
value="publish"
<?php checked($default_post_status, 'publish'); ?>
/>
<strong><?php _e('Publish', 'igny8-bridge'); ?></strong>
<span class="description"><?php _e('Publish content immediately when received from IGNY8.', 'igny8-bridge'); ?></span>
</label>
<p class="description" style="margin-top: 8px;">
<?php _e('Choose whether content published from IGNY8 should be saved as draft or published immediately in WordPress.', 'igny8-bridge'); ?>
</p>
</div>
</div>
<div class="automation-column-right">
<div class="automation-block">
<h3><?php _e('WooCommerce Products', 'igny8-bridge'); ?></h3>
<label>
<input
type="checkbox"
name="igny8_enable_woocommerce"
value="1"
<?php checked($woocommerce_enabled, 1); ?>
<?php disabled(!$woocommerce_detected); ?>
/>
<?php _e('Sync WooCommerce products and categories.', 'igny8-bridge'); ?>
</label>
<?php if (!$woocommerce_detected) : ?>
<p class="description">
<?php _e('WooCommerce is not active on this site. Enable the plugin to sync product data.', 'igny8-bridge'); ?>
</p>
<?php endif; ?>
</div>
<div class="automation-block">
<h3><?php _e('Taxonomies to Sync', 'igny8-bridge'); ?></h3>
<div class="taxonomy-list">
<?php foreach ($available_taxonomies as $taxonomy_slug => $taxonomy_label) : ?>
<label class="taxonomy-item">
<input
type="checkbox"
name="igny8_enabled_taxonomies[]"
value="<?php echo esc_attr($taxonomy_slug); ?>"
<?php checked(in_array($taxonomy_slug, $enabled_taxonomies, true)); ?>
/>
<?php echo esc_html($taxonomy_label); ?> <span class="taxonomy-slug">(<?php echo esc_html($taxonomy_slug); ?>)</span>
</label>
<?php endforeach; ?>
</div>
<p class="description">
<?php _e('Select which taxonomies to synchronize bidirectionally with IGNY8.', 'igny8-bridge'); ?>
</p>
</div>
<div class="automation-block" style="margin-top:18px;">
<?php submit_button(__('Save Automation Settings', 'igny8-bridge')); ?>
</div>
</div>
</div>
</form>
<p class="description">
<?php _e('Once these settings are saved, the bridge schedules automatic jobs that keep IGNY8 in sync without manual actions.', 'igny8-bridge'); ?>
</p>
</div>
<?php if ($is_connected) : ?>
<div class="igny8-settings-card">
<h2>
<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="vertical-align: middle; margin-right: 8px; display: inline;">
<path stroke-linecap="round" stroke-linejoin="round" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10"/>
</svg>
<?php _e('Sync Operations', 'igny8-bridge'); ?>
</h2>
<?php if (!$connection_enabled) : ?>
<div class="notice notice-warning inline">
<p>
<strong><?php _e('Connection Disabled', 'igny8-bridge'); ?></strong><br />
<?php _e('Sync operations are currently disabled. Enable "Enable Communication" above to use these features.', 'igny8-bridge'); ?>
</p>
</div>
<?php endif; ?>
<?php
// Get counts for sync operations
$post_count = wp_count_posts('post');
$page_count = wp_count_posts('page');
$product_count = class_exists('WooCommerce') ? wp_count_posts('product') : null;
$total_posts = $post_count->publish + $page_count->publish;
if ($product_count) $total_posts += $product_count->publish;
$taxonomies = get_taxonomies(['public' => true], 'objects');
$taxonomy_count = 0;
foreach ($taxonomies as $taxonomy) {
$taxonomy_count += wp_count_terms(['taxonomy' => $taxonomy->name, 'hide_empty' => false]);
}
?>
<div class="igny8-sync-grid">
<div class="igny8-sync-card">
<div class="igny8-sync-icon">
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
</div>
<h3><?php _e('Sync Posts to IGNY8', 'igny8-bridge'); ?></h3>
<p class="igny8-sync-description">
<?php printf(
__('Send %d posts, %d pages%s from WordPress to IGNY8', 'igny8-bridge'),
$post_count->publish,
$page_count->publish,
$product_count ? sprintf(', %d products', $product_count->publish) : ''
); ?>
</p>
<p class="igny8-sync-meta">
<?php if ($last_site_sync) : ?>
<span class="igny8-sync-time">⏱ <?php echo sprintf(__('Last sync: %s', 'igny8-bridge'), human_time_diff($last_site_sync, current_time('timestamp')) . ' ago'); ?></span>
<?php else : ?>
<span class="igny8-sync-time">⏱ <?php _e('Never synced', 'igny8-bridge'); ?></span>
<?php endif; ?>
</p>
<button type="button" id="igny8-sync-posts" class="button button-primary igny8-sync-button" <?php disabled(!$connection_enabled); ?>>
<span class="button-text"><?php _e('Sync Now', 'igny8-bridge'); ?></span>
<span class="button-loading" style="display: none;">
<span class="spinner is-active"></span> <?php _e('Syncing...', 'igny8-bridge'); ?>
</span>
</button>
</div>
<div class="igny8-sync-card">
<div class="igny8-sync-icon">
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/>
</svg>
</div>
<h3><?php _e('Sync Taxonomies', 'igny8-bridge'); ?></h3>
<p class="igny8-sync-description">
<?php printf(
__('Sync %d taxonomy terms (categories, tags, sectors, clusters) to IGNY8', 'igny8-bridge'),
$taxonomy_count
); ?>
</p>
<p class="igny8-sync-meta">
<?php if ($last_taxonomy_sync) : ?>
<span class="igny8-sync-time">⏱ <?php echo sprintf(__('Last sync: %s', 'igny8-bridge'), human_time_diff($last_taxonomy_sync, current_time('timestamp')) . ' ago'); ?></span>
<?php else : ?>
<span class="igny8-sync-time">⏱ <?php _e('Never synced', 'igny8-bridge'); ?></span>
<?php endif; ?>
</p>
<button type="button" id="igny8-sync-taxonomies" class="button button-primary igny8-sync-button" <?php disabled(!$connection_enabled); ?>>
<span class="button-text"><?php _e('Sync Now', 'igny8-bridge'); ?></span>
<span class="button-loading" style="display: none;">
<span class="spinner is-active"></span> <?php _e('Syncing...', 'igny8-bridge'); ?>
</span>
</button>
</div>
<div class="igny8-sync-card">
<div class="igny8-sync-icon">
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/>
</svg>
</div>
<h3><?php _e('Sync from IGNY8', 'igny8-bridge'); ?></h3>
<p class="igny8-sync-description">
<?php _e('Import new content, tasks, and updates from IGNY8 AI OS to WordPress', 'igny8-bridge'); ?>
</p>
<p class="igny8-sync-meta">
<?php if ($last_writer_sync) : ?>
<span class="igny8-sync-time">⏱ <?php echo sprintf(__('Last sync: %s', 'igny8-bridge'), human_time_diff($last_writer_sync, current_time('timestamp')) . ' ago'); ?></span>
<?php else : ?>
<span class="igny8-sync-time">⏱ <?php _e('Never synced', 'igny8-bridge'); ?></span>
<?php endif; ?>
</p>
<button type="button" id="igny8-sync-from-igny8" class="button button-primary igny8-sync-button" <?php disabled(!$connection_enabled); ?>>
<span class="button-text"><?php _e('Sync Now', 'igny8-bridge'); ?></span>
<span class="button-loading" style="display: none;">
<span class="spinner is-active"></span> <?php _e('Syncing...', 'igny8-bridge'); ?>
</span>
</button>
</div>
<div class="igny8-sync-card igny8-sync-card-highlight">
<div class="igny8-sync-icon">
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/>
</svg>
</div>
<h3><?php _e('Collect & Send Site Data', 'igny8-bridge'); ?></h3>
<p class="igny8-sync-description">
<?php _e('Perform full site scan: collect structure, links, SEO data, and send comprehensive report to IGNY8', 'igny8-bridge'); ?>
</p>
<p class="igny8-sync-meta">
<?php if ($last_full_site_scan) : ?>
<span class="igny8-sync-time">⏱ <?php echo sprintf(__('Last scan: %s', 'igny8-bridge'), human_time_diff($last_full_site_scan, current_time('timestamp')) . ' ago'); ?></span>
<?php else : ?>
<span class="igny8-sync-time">⏱ <?php _e('Never scanned', 'igny8-bridge'); ?></span>
<?php endif; ?>
</p>
<button type="button" id="igny8-collect-site-data" class="button button-primary igny8-sync-button" <?php disabled(!$connection_enabled); ?>>
<span class="button-text"><?php _e('Start Full Scan', 'igny8-bridge'); ?></span>
<span class="button-loading" style="display: none;">
<span class="spinner is-active"></span> <?php _e('Scanning...', 'igny8-bridge'); ?>
</span>
</button>
</div>
</div>
<div id="igny8-sync-status" class="igny8-sync-status"></div>
</div>
<div class="igny8-settings-card">
<h2><?php _e('Sync Statistics', 'igny8-bridge'); ?></h2>
<div class="igny8-stats-grid">
<div class="igny8-stat-card">
<div class="igny8-stat-icon" style="background-color: #3B82F6;">
<svg width="20" height="20" fill="none" stroke="white" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
</div>
<div class="igny8-stat-content">
<div class="igny8-stat-label"><?php _e('Synced Posts', 'igny8-bridge'); ?></div>
<div class="igny8-stat-value" id="igny8-stat-posts"><?php echo number_format($total_posts); ?></div>
<div class="igny8-stat-meta"><?php printf(__('%d posts · %d pages', 'igny8-bridge'), $post_count->publish, $page_count->publish); ?></div>
</div>
</div>
<div class="igny8-stat-card">
<div class="igny8-stat-icon" style="background-color: #10B981;">
<svg width="20" height="20" fill="none" stroke="white" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/>
</svg>
</div>
<div class="igny8-stat-content">
<div class="igny8-stat-label"><?php _e('Taxonomy Terms', 'igny8-bridge'); ?></div>
<div class="igny8-stat-value" id="igny8-stat-taxonomies"><?php echo number_format($taxonomy_count); ?></div>
<div class="igny8-stat-meta"><?php _e('Categories, tags, sectors', 'igny8-bridge'); ?></div>
</div>
</div>
<div class="igny8-stat-card">
<div class="igny8-stat-icon" style="background-color: #F59E0B;">
<svg width="20" height="20" fill="none" stroke="white" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div class="igny8-stat-content">
<div class="igny8-stat-label"><?php _e('Last Sync', 'igny8-bridge'); ?></div>
<div class="igny8-stat-value" id="igny8-stat-last-sync">
<?php if ($last_site_sync) : ?>
<?php echo human_time_diff($last_site_sync, current_time('timestamp')); ?> <?php _e('ago', 'igny8-bridge'); ?>
<?php else : ?>
<?php _e('Never', 'igny8-bridge'); ?>
<?php endif; ?>
</div>
<div class="igny8-stat-meta">
<?php if ($last_site_sync) : ?>
<?php echo date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $last_site_sync); ?>
<?php else : ?>
<?php _e('No sync performed yet', 'igny8-bridge'); ?>
<?php endif; ?>
</div>
</div>
</div>
<div class="igny8-stat-card">
<div class="igny8-stat-icon" style="background-color: #8B5CF6;">
<svg width="20" height="20" fill="none" stroke="white" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
</div>
<div class="igny8-stat-content">
<div class="igny8-stat-label"><?php _e('Connection Status', 'igny8-bridge'); ?></div>
<div class="igny8-stat-value">
<?php if ($connection_enabled) : ?>
<span style="color: #10B981; font-size: 18px; font-weight: 600;"><?php _e('Active', 'igny8-bridge'); ?></span>
<?php else : ?>
<span style="color: #6B7280; font-size: 18px; font-weight: 600;"><?php _e('Disabled', 'igny8-bridge'); ?></span>
<?php endif; ?>
</div>
<div class="igny8-stat-meta">
<?php if ($last_health_check) : ?>
<?php _e('Last checked:', 'igny8-bridge'); ?> <?php echo human_time_diff($last_health_check, current_time('timestamp')); ?> <?php _e('ago', 'igny8-bridge'); ?>
<?php else : ?>
<?php _e('Health check pending', 'igny8-bridge'); ?>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php if ($last_semantic_map && !empty($semantic_summary)) : ?>
<div class="igny8-semantic-summary">
<h3><?php _e('Latest Site Analysis', 'igny8-bridge'); ?></h3>
<div class="igny8-semantic-stats">
<div class="igny8-semantic-stat">
<span class="value"><?php echo number_format($semantic_summary['sectors'] ?? 0); ?></span>
<span class="label"><?php _e('Sectors', 'igny8-bridge'); ?></span>
</div>
<div class="igny8-semantic-stat">
<span class="value"><?php echo number_format($semantic_summary['keywords'] ?? 0); ?></span>
<span class="label"><?php _e('Keywords', 'igny8-bridge'); ?></span>
</div>
<div class="igny8-semantic-stat">
<span class="value"><?php echo human_time_diff($last_semantic_map, current_time('timestamp')); ?></span>
<span class="label"><?php _e('ago', 'igny8-bridge'); ?></span>
</div>
</div>
</div>
<?php endif; ?>
<!-- Diagnostics (moved to near end) -->
<div class="igny8-settings-card">
<h2>
<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="vertical-align: middle; margin-right: 8px; display: inline;">
<path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<?php _e('Diagnostics', 'igny8-bridge'); ?>
</h2>
<div class="igny8-diagnostics-grid">
<div class="igny8-diagnostic-item">
<div class="igny8-diagnostic-label"><?php _e('Access Token Age', 'igny8-bridge'); ?></div>
<div class="igny8-diagnostic-value"><?php echo esc_html($token_age_text); ?></div>
<p class="description"><?php echo esc_html($token_issued_formatted); ?></p>
</div>
<div class="igny8-diagnostic-item">
<div class="igny8-diagnostic-label"><?php _e('Last API Health Check', 'igny8-bridge'); ?></div>
<div class="igny8-diagnostic-value"><?php echo esc_html($last_health_check_formatted); ?></div>
<p class="description"><?php _e('Updated when tests succeed', 'igny8-bridge'); ?></p>
</div>
<div class="igny8-diagnostic-item">
<div class="igny8-diagnostic-label"><?php _e('Last Site Data Sync', 'igny8-bridge'); ?></div>
<div class="igny8-diagnostic-value"><?php echo esc_html($last_site_sync_formatted); ?></div>
<p class="description"><?php _e('Triggered automatically after setup', 'igny8-bridge'); ?></p>
</div>
<div class="igny8-diagnostic-item">
<div class="igny8-diagnostic-label"><?php _e('Last Full Site Scan', 'igny8-bridge'); ?></div>
<div class="igny8-diagnostic-value"><?php echo esc_html($last_full_site_scan_formatted); ?></div>
<p class="description"><?php _e('Runs weekly or when requested manually', 'igny8-bridge'); ?></p>
</div>
<div class="igny8-diagnostic-item">
<div class="igny8-diagnostic-label"><?php _e('Last Semantic Map', 'igny8-bridge'); ?></div>
<div class="igny8-diagnostic-value"><?php echo esc_html($last_semantic_map_formatted); ?></div>
<p class="description">
<?php
if (!empty($semantic_summary)) {
printf(
esc_html__('Sectors: %1$d · Keywords: %2$d', 'igny8-bridge'),
intval($semantic_summary['sectors'] ?? 0),
intval($semantic_summary['keywords'] ?? 0)
);
} else {
_e('Planner semantic map generated after full scan', 'igny8-bridge');
}
?>
</p>
</div>
<div class="igny8-diagnostic-item">
<div class="igny8-diagnostic-label"><?php _e('Last Taxonomy Sync', 'igny8-bridge'); ?></div>
<div class="igny8-diagnostic-value"><?php echo esc_html($last_taxonomy_sync_formatted); ?></div>
<p class="description"><?php _e('Sectors & clusters imported from Planner', 'igny8-bridge'); ?></p>
</div>
<div class="igny8-diagnostic-item">
<div class="igny8-diagnostic-label"><?php _e('Last Keyword Sync', 'igny8-bridge'); ?></div>
<div class="igny8-diagnostic-value"><?php echo esc_html($last_keyword_sync_formatted); ?></div>
<p class="description"><?php _e('Planner keywords cached to WordPress posts', 'igny8-bridge'); ?></p>
</div>
<div class="igny8-diagnostic-item">
<div class="igny8-diagnostic-label"><?php _e('Last Writer Sync', 'igny8-bridge'); ?></div>
<div class="igny8-diagnostic-value"><?php echo esc_html($last_writer_sync_formatted); ?></div>
<p class="description"><?php _e('New IGNY8 tasks imported automatically', 'igny8-bridge'); ?></p>
</div>
<div class="igny8-diagnostic-item">
<div class="igny8-diagnostic-label"><?php _e('Next Scheduled Site Scan', 'igny8-bridge'); ?></div>
<div class="igny8-diagnostic-value"><?php echo esc_html($next_site_sync_formatted); ?></div>
</div>
</div>
</div>
<!-- About (moved to the end) -->
<div class="igny8-settings-card">
<h2><?php _e('About', 'igny8-bridge'); ?></h2>
<p><?php _e('The IGNY8 WordPress Bridge plugin connects your WordPress site to the IGNY8 API, enabling two-way synchronization of posts, taxonomies, and site data.', 'igny8-bridge'); ?></p>
<p><strong><?php _e('Version:', 'igny8-bridge'); ?></strong> <?php echo esc_html(IGNY8_BRIDGE_VERSION); ?></p>
<p><strong><?php _e('Authentication:', 'igny8-bridge'); ?></strong> <?php _e('API Key Only (secure, modern authentication)', 'igny8-bridge'); ?></p>
</div>
</div>
<?php endif; ?>
</div>
</div>