wp
This commit is contained in:
2
igny8-wp-plugin/.gitattributes
vendored
2
igny8-wp-plugin/.gitattributes
vendored
@@ -1,2 +0,0 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
@@ -1,847 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
/**
|
||||
* 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);
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
/**
|
||||
* 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);
|
||||
|
||||
@@ -1,255 +0,0 @@
|
||||
<?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'));
|
||||
|
||||
@@ -1,651 +0,0 @@
|
||||
<?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'));
|
||||
|
||||
@@ -1,413 +0,0 @@
|
||||
<?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();
|
||||
@@ -1,469 +0,0 @@
|
||||
<?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();
|
||||
|
||||
@@ -1,716 +0,0 @@
|
||||
<?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>
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Link Graph Collection
|
||||
*
|
||||
* Extracts WordPress internal link graph for IGNY8 Linker module
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract internal links from post content
|
||||
*
|
||||
* @param int $post_id Post ID
|
||||
* @return array Array of link objects with source_url, target_url, anchor
|
||||
*/
|
||||
function igny8_extract_post_links($post_id) {
|
||||
$post = get_post($post_id);
|
||||
if (!$post) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$content = $post->post_content;
|
||||
$source_url = get_permalink($post_id);
|
||||
$site_url = get_site_url();
|
||||
$links = array();
|
||||
|
||||
// Match all anchor tags with href attributes
|
||||
preg_match_all('/<a[^>]+href=["\']([^"\']+)["\'][^>]*>(.*?)<\/a>/is', $content, $matches, PREG_SET_ORDER);
|
||||
|
||||
foreach ($matches as $match) {
|
||||
$href = $match[1];
|
||||
$anchor = strip_tags($match[2]);
|
||||
|
||||
// Skip empty anchors
|
||||
if (empty(trim($anchor))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only process internal links
|
||||
if (strpos($href, $site_url) === 0 || strpos($href, '/') === 0) {
|
||||
// Convert relative URLs to absolute
|
||||
if (strpos($href, '/') === 0 && strpos($href, '//') !== 0) {
|
||||
$href = $site_url . $href;
|
||||
}
|
||||
|
||||
// Normalize URL (remove trailing slash, fragments, query params for matching)
|
||||
$target_url = rtrim($href, '/');
|
||||
|
||||
// Skip if source and target are the same
|
||||
if ($source_url === $target_url) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$links[] = array(
|
||||
'source_url' => $source_url,
|
||||
'target_url' => $target_url,
|
||||
'anchor' => trim($anchor),
|
||||
'post_id' => $post_id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $links;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract link graph from all posts
|
||||
*
|
||||
* @param array $post_ids Optional array of post IDs to process. If empty, processes all enabled posts.
|
||||
* @return array Link graph array
|
||||
*/
|
||||
function igny8_extract_link_graph($post_ids = array()) {
|
||||
// Skip if connection is disabled
|
||||
if (!igny8_is_connection_enabled()) {
|
||||
return array();
|
||||
}
|
||||
|
||||
if (function_exists('igny8_is_module_enabled') && !igny8_is_module_enabled('linker')) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$enabled_post_types = igny8_get_enabled_post_types();
|
||||
|
||||
if (empty($post_ids)) {
|
||||
// Get all published posts of enabled types
|
||||
$query_args = array(
|
||||
'post_type' => $enabled_post_types,
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'fields' => 'ids',
|
||||
'suppress_filters' => true
|
||||
);
|
||||
|
||||
$post_ids = get_posts($query_args);
|
||||
}
|
||||
|
||||
$link_graph = array();
|
||||
$processed = 0;
|
||||
|
||||
foreach ($post_ids as $post_id) {
|
||||
$links = igny8_extract_post_links($post_id);
|
||||
|
||||
if (!empty($links)) {
|
||||
$link_graph = array_merge($link_graph, $links);
|
||||
}
|
||||
|
||||
$processed++;
|
||||
|
||||
// Limit processing to prevent timeout (can be increased or made configurable)
|
||||
if ($processed >= 1000) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $link_graph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send link graph to IGNY8 Linker module
|
||||
*
|
||||
* @param int $site_id IGNY8 site ID
|
||||
* @param array $link_graph Link graph array (optional, will extract if not provided)
|
||||
* @return array|false Response data or false on failure
|
||||
*/
|
||||
function igny8_send_link_graph_to_igny8($site_id, $link_graph = null) {
|
||||
// Skip if connection is disabled
|
||||
if (!igny8_is_connection_enabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (function_exists('igny8_is_module_enabled') && !igny8_is_module_enabled('linker')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$api = new Igny8API();
|
||||
|
||||
if (!$api->is_authenticated()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract link graph if not provided
|
||||
if ($link_graph === null) {
|
||||
$link_graph = igny8_extract_link_graph();
|
||||
}
|
||||
|
||||
if (empty($link_graph)) {
|
||||
return array('success' => true, 'message' => 'No links found', 'links_count' => 0);
|
||||
}
|
||||
|
||||
// Send in batches (max 500 links per batch)
|
||||
$batch_size = 500;
|
||||
$batches = array_chunk($link_graph, $batch_size);
|
||||
$total_sent = 0;
|
||||
$errors = array();
|
||||
|
||||
foreach ($batches as $batch) {
|
||||
$response = $api->post("/linker/link-map/", array(
|
||||
'site_id' => $site_id,
|
||||
'links' => $batch,
|
||||
'total_links' => count($link_graph),
|
||||
'batch_number' => count($batches) > 1 ? (count($batches) - count($batches) + array_search($batch, $batches) + 1) : 1,
|
||||
'total_batches' => count($batches)
|
||||
));
|
||||
|
||||
if ($response['success']) {
|
||||
$total_sent += count($batch);
|
||||
} else {
|
||||
$errors[] = $response['error'] ?? 'Unknown error';
|
||||
}
|
||||
}
|
||||
|
||||
if ($total_sent > 0) {
|
||||
update_option('igny8_last_link_graph_sync', current_time('timestamp'));
|
||||
update_option('igny8_last_link_graph_count', $total_sent);
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'links_sent' => $total_sent,
|
||||
'total_links' => count($link_graph),
|
||||
'batches' => count($batches),
|
||||
'errors' => $errors
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Semantic Strategy Mapping
|
||||
*
|
||||
* Maps WordPress site data to IGNY8 semantic structure
|
||||
* Follows WORDPRESS-PLUGIN-INTEGRATION.md guidelines
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map WordPress site data to IGNY8 semantic strategy
|
||||
* This creates sectors, clusters, and keywords based on site structure
|
||||
*
|
||||
* @param int $site_id IGNY8 site ID
|
||||
* @param array $site_data Site data from igny8_collect_site_data()
|
||||
* @return array Response from IGNY8 API
|
||||
*/
|
||||
function igny8_map_site_to_semantic_strategy($site_id, $site_data) {
|
||||
// Skip if connection is disabled
|
||||
if (!igny8_is_connection_enabled()) {
|
||||
return array('success' => false, 'error' => 'Connection disabled');
|
||||
}
|
||||
|
||||
$api = new Igny8API();
|
||||
|
||||
if (!$api->is_authenticated()) {
|
||||
return array('success' => false, 'error' => 'Not authenticated');
|
||||
}
|
||||
|
||||
// Extract semantic structure from site data
|
||||
$semantic_map = array(
|
||||
'sectors' => array(),
|
||||
'clusters' => array(),
|
||||
'keywords' => array()
|
||||
);
|
||||
|
||||
// Map taxonomies to sectors
|
||||
foreach ($site_data['taxonomies'] as $tax_name => $tax_data) {
|
||||
if ($tax_data['taxonomy']['hierarchical']) {
|
||||
// Hierarchical taxonomies (categories) become sectors
|
||||
$sector = array(
|
||||
'name' => $tax_data['taxonomy']['label'],
|
||||
'slug' => $tax_data['taxonomy']['name'],
|
||||
'description' => $tax_data['taxonomy']['description'],
|
||||
'source' => 'wordpress_taxonomy',
|
||||
'source_id' => $tax_name
|
||||
);
|
||||
|
||||
// Map terms to clusters
|
||||
$clusters = array();
|
||||
foreach ($tax_data['terms'] as $term) {
|
||||
$clusters[] = array(
|
||||
'name' => $term['name'],
|
||||
'slug' => $term['slug'],
|
||||
'description' => $term['description'],
|
||||
'source' => 'wordpress_term',
|
||||
'source_id' => $term['id']
|
||||
);
|
||||
|
||||
// Extract keywords from posts in this term
|
||||
$keywords = igny8_extract_keywords_from_term_posts($term['id'], $tax_name);
|
||||
$semantic_map['keywords'] = array_merge($semantic_map['keywords'], $keywords);
|
||||
}
|
||||
|
||||
$sector['clusters'] = $clusters;
|
||||
$semantic_map['sectors'][] = $sector;
|
||||
}
|
||||
}
|
||||
|
||||
// Map WooCommerce product categories to sectors
|
||||
if (!empty($site_data['product_categories'])) {
|
||||
$product_sector = array(
|
||||
'name' => 'Products',
|
||||
'slug' => 'products',
|
||||
'description' => 'WooCommerce product categories',
|
||||
'source' => 'woocommerce',
|
||||
'clusters' => array()
|
||||
);
|
||||
|
||||
foreach ($site_data['product_categories'] as $category) {
|
||||
$product_sector['clusters'][] = array(
|
||||
'name' => $category['name'],
|
||||
'slug' => $category['slug'],
|
||||
'description' => $category['description'],
|
||||
'source' => 'woocommerce_category',
|
||||
'source_id' => $category['id']
|
||||
);
|
||||
}
|
||||
|
||||
$semantic_map['sectors'][] = $product_sector;
|
||||
}
|
||||
|
||||
// Send semantic map to IGNY8
|
||||
$response = $api->post("/planner/sites/{$site_id}/semantic-map/", array(
|
||||
'semantic_map' => $semantic_map,
|
||||
'site_data' => $site_data
|
||||
));
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract keywords from posts associated with a taxonomy term
|
||||
*
|
||||
* @param int $term_id Term ID
|
||||
* @param string $taxonomy Taxonomy name
|
||||
* @return array Formatted keywords array
|
||||
*/
|
||||
function igny8_extract_keywords_from_term_posts($term_id, $taxonomy) {
|
||||
$args = array(
|
||||
'post_type' => 'any',
|
||||
'posts_per_page' => -1,
|
||||
'tax_query' => array(
|
||||
array(
|
||||
'taxonomy' => $taxonomy,
|
||||
'field' => 'term_id',
|
||||
'terms' => $term_id
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
$query = new WP_Query($args);
|
||||
$keywords = array();
|
||||
|
||||
if ($query->have_posts()) {
|
||||
while ($query->have_posts()) {
|
||||
$query->the_post();
|
||||
|
||||
// Extract keywords from post title and content
|
||||
$title_words = str_word_count(get_the_title(), 1);
|
||||
$content_words = str_word_count(strip_tags(get_the_content()), 1);
|
||||
|
||||
// Combine and get unique keywords
|
||||
$all_words = array_merge($title_words, $content_words);
|
||||
$unique_words = array_unique(array_map('strtolower', $all_words));
|
||||
|
||||
// Filter out common words (stop words)
|
||||
$stop_words = array('the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by');
|
||||
$keywords = array_merge($keywords, array_diff($unique_words, $stop_words));
|
||||
}
|
||||
wp_reset_postdata();
|
||||
}
|
||||
|
||||
// Format keywords
|
||||
$formatted_keywords = array();
|
||||
foreach (array_unique($keywords) as $keyword) {
|
||||
if (strlen($keyword) > 3) { // Only keywords longer than 3 characters
|
||||
$formatted_keywords[] = array(
|
||||
'keyword' => $keyword,
|
||||
'source' => 'wordpress_post',
|
||||
'source_term_id' => $term_id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $formatted_keywords;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete workflow: Fetch site data → Map to semantic strategy → Restructure content
|
||||
*
|
||||
* @param int $site_id IGNY8 site ID
|
||||
* @return array|false Analysis result or false on failure
|
||||
*/
|
||||
function igny8_analyze_and_restructure_site($site_id) {
|
||||
$api = new Igny8API();
|
||||
|
||||
if (!$api->is_authenticated()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 1: Collect all site data
|
||||
$site_data = igny8_collect_site_data();
|
||||
|
||||
// Step 2: Send to IGNY8 for analysis
|
||||
$analysis_response = $api->post("/system/sites/{$site_id}/analyze/", array(
|
||||
'site_data' => $site_data,
|
||||
'analysis_type' => 'full_site_restructure'
|
||||
));
|
||||
|
||||
if (!$analysis_response['success']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$analysis_id = $analysis_response['data']['analysis_id'] ?? null;
|
||||
|
||||
// Step 3: Map to semantic strategy
|
||||
$mapping_response = igny8_map_site_to_semantic_strategy($site_id, $site_data);
|
||||
|
||||
if (!$mapping_response['success']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 4: Get restructuring recommendations
|
||||
$recommendations_response = $api->get("/system/sites/{$site_id}/recommendations/");
|
||||
|
||||
if (!$recommendations_response['success']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get keywords count from mapping response
|
||||
$keywords_count = 0;
|
||||
if (isset($mapping_response['data']['keywords'])) {
|
||||
$keywords_count = count($mapping_response['data']['keywords']);
|
||||
}
|
||||
|
||||
return array(
|
||||
'analysis_id' => $analysis_id,
|
||||
'semantic_map' => $mapping_response['data'] ?? null,
|
||||
'recommendations' => $recommendations_response['data'] ?? null,
|
||||
'site_data_summary' => array(
|
||||
'total_posts' => count($site_data['posts']),
|
||||
'total_taxonomies' => count($site_data['taxonomies']),
|
||||
'total_products' => count($site_data['products'] ?? array()),
|
||||
'total_keywords' => $keywords_count
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,601 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* WordPress Site Data Collection
|
||||
*
|
||||
* Collects WordPress posts, taxonomies, and site data for IGNY8
|
||||
* Follows WORDPRESS-PLUGIN-INTEGRATION.md guidelines
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all posts of a specific type from WordPress
|
||||
*
|
||||
* @param string $post_type Post type
|
||||
* @param int $per_page Posts per page
|
||||
* @return array|false Formatted posts array or false on failure
|
||||
*/
|
||||
function igny8_fetch_wordpress_posts($post_type = 'post', $per_page = 100, $args = array()) {
|
||||
$defaults = array(
|
||||
'status' => 'publish',
|
||||
'after' => null,
|
||||
'max_pages' => 5,
|
||||
);
|
||||
$args = wp_parse_args($args, $defaults);
|
||||
|
||||
$post_type_object = get_post_type_object($post_type);
|
||||
$rest_base = ($post_type_object && !empty($post_type_object->rest_base)) ? $post_type_object->rest_base : $post_type;
|
||||
|
||||
$base_url = sprintf('%s/wp-json/wp/v2/%s', get_site_url(), $rest_base);
|
||||
|
||||
$query_args = array(
|
||||
'per_page' => min($per_page, 100),
|
||||
'status' => $args['status'],
|
||||
'orderby' => 'modified',
|
||||
'order' => 'desc',
|
||||
);
|
||||
|
||||
if (!empty($args['after'])) {
|
||||
$query_args['after'] = gmdate('c', $args['after']);
|
||||
}
|
||||
|
||||
$formatted_posts = array();
|
||||
$page = 1;
|
||||
|
||||
do {
|
||||
$query_args['page'] = $page;
|
||||
$response = wp_remote_get(add_query_arg($query_args, $base_url));
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$posts = json_decode(wp_remote_retrieve_body($response), true);
|
||||
|
||||
if (!is_array($posts) || empty($posts)) {
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($posts as $post) {
|
||||
$content = $post['content']['rendered'] ?? '';
|
||||
$word_count = str_word_count(strip_tags($content));
|
||||
|
||||
$formatted_posts[] = array(
|
||||
'id' => $post['id'],
|
||||
'title' => html_entity_decode($post['title']['rendered'] ?? ''),
|
||||
'content' => $content,
|
||||
'excerpt' => $post['excerpt']['rendered'] ?? '',
|
||||
'status' => $post['status'] ?? 'draft',
|
||||
'url' => $post['link'] ?? '',
|
||||
'published' => $post['date'] ?? '',
|
||||
'modified' => $post['modified'] ?? '',
|
||||
'author' => $post['author'] ?? 0,
|
||||
'post_type' => $post['type'] ?? $post_type,
|
||||
'taxonomies' => array(
|
||||
'categories' => $post['categories'] ?? array(),
|
||||
'tags' => $post['tags'] ?? array(),
|
||||
),
|
||||
'meta' => array(
|
||||
'word_count' => $word_count,
|
||||
'reading_time' => $word_count ? ceil($word_count / 200) : 0,
|
||||
'featured_media' => $post['featured_media'] ?? 0,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (count($posts) < $query_args['per_page']) {
|
||||
break;
|
||||
}
|
||||
|
||||
$page++;
|
||||
} while ($page <= $args['max_pages']);
|
||||
|
||||
return $formatted_posts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all available post types from WordPress
|
||||
*
|
||||
* @return array|false Post types array or false on failure
|
||||
*/
|
||||
function igny8_fetch_all_post_types() {
|
||||
$wp_response = wp_remote_get(get_site_url() . '/wp-json/wp/v2/types');
|
||||
|
||||
if (is_wp_error($wp_response)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$types = json_decode(wp_remote_retrieve_body($wp_response), true);
|
||||
|
||||
if (!is_array($types)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$post_types = array();
|
||||
foreach ($types as $type_name => $type_data) {
|
||||
if ($type_data['public']) {
|
||||
$post_types[] = array(
|
||||
'name' => $type_name,
|
||||
'label' => $type_data['name'],
|
||||
'description' => $type_data['description'] ?? '',
|
||||
'rest_base' => $type_data['rest_base'] ?? $type_name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $post_types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all posts from all post types
|
||||
*
|
||||
* @param int $per_page Posts per page
|
||||
* @return array All posts
|
||||
*/
|
||||
function igny8_fetch_all_wordpress_posts($per_page = 100) {
|
||||
$post_types = igny8_fetch_all_post_types();
|
||||
|
||||
if (!$post_types) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$all_posts = array();
|
||||
foreach ($post_types as $type) {
|
||||
$posts = igny8_fetch_wordpress_posts($type['name'], $per_page);
|
||||
if ($posts) {
|
||||
$all_posts = array_merge($all_posts, $posts);
|
||||
}
|
||||
}
|
||||
|
||||
return $all_posts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all taxonomies from WordPress
|
||||
*
|
||||
* @return array|false Taxonomies array or false on failure
|
||||
*/
|
||||
function igny8_fetch_wordpress_taxonomies() {
|
||||
$wp_response = wp_remote_get(get_site_url() . '/wp-json/wp/v2/taxonomies');
|
||||
|
||||
if (is_wp_error($wp_response)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$taxonomies = json_decode(wp_remote_retrieve_body($wp_response), true);
|
||||
|
||||
if (!is_array($taxonomies)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$formatted_taxonomies = array();
|
||||
foreach ($taxonomies as $tax_name => $tax_data) {
|
||||
if ($tax_data['public']) {
|
||||
$formatted_taxonomies[] = array(
|
||||
'name' => $tax_name,
|
||||
'label' => $tax_data['name'],
|
||||
'description' => $tax_data['description'] ?? '',
|
||||
'hierarchical' => $tax_data['hierarchical'],
|
||||
'rest_base' => $tax_data['rest_base'] ?? $tax_name,
|
||||
'object_types' => $tax_data['types'] ?? array()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $formatted_taxonomies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all terms for a specific taxonomy
|
||||
*
|
||||
* @param string $taxonomy Taxonomy name
|
||||
* @param int $per_page Terms per page
|
||||
* @return array|false Formatted terms array or false on failure
|
||||
*/
|
||||
function igny8_fetch_taxonomy_terms($taxonomy, $per_page = 100) {
|
||||
$taxonomy_obj = get_taxonomy($taxonomy);
|
||||
$rest_base = ($taxonomy_obj && !empty($taxonomy_obj->rest_base)) ? $taxonomy_obj->rest_base : $taxonomy;
|
||||
|
||||
$base_url = sprintf('%s/wp-json/wp/v2/%s', get_site_url(), $rest_base);
|
||||
|
||||
$formatted_terms = array();
|
||||
$page = 1;
|
||||
|
||||
do {
|
||||
$response = wp_remote_get(add_query_arg(array(
|
||||
'per_page' => min($per_page, 100),
|
||||
'page' => $page
|
||||
), $base_url));
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$terms = json_decode(wp_remote_retrieve_body($response), true);
|
||||
|
||||
if (!is_array($terms) || empty($terms)) {
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($terms as $term) {
|
||||
$formatted_terms[] = array(
|
||||
'id' => $term['id'],
|
||||
'name' => $term['name'],
|
||||
'slug' => $term['slug'],
|
||||
'description' => $term['description'] ?? '',
|
||||
'count' => $term['count'],
|
||||
'parent' => $term['parent'] ?? 0,
|
||||
'taxonomy' => $taxonomy,
|
||||
'url' => $term['link'] ?? ''
|
||||
);
|
||||
}
|
||||
|
||||
if (count($terms) < min($per_page, 100)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$page++;
|
||||
} while (true);
|
||||
|
||||
return $formatted_terms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all terms from all taxonomies
|
||||
*
|
||||
* @param int $per_page Terms per page
|
||||
* @return array All terms organized by taxonomy
|
||||
*/
|
||||
function igny8_fetch_all_taxonomy_terms($per_page = 100) {
|
||||
$taxonomies = igny8_fetch_wordpress_taxonomies();
|
||||
|
||||
if (!$taxonomies) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$all_terms = array();
|
||||
foreach ($taxonomies as $taxonomy) {
|
||||
$terms = igny8_fetch_taxonomy_terms($taxonomy['rest_base'], $per_page);
|
||||
if ($terms) {
|
||||
$all_terms[$taxonomy['name']] = $terms;
|
||||
}
|
||||
}
|
||||
|
||||
return $all_terms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all WordPress site data for IGNY8 semantic mapping
|
||||
*
|
||||
* @return array Complete site data
|
||||
*/
|
||||
function igny8_collect_site_data($args = array()) {
|
||||
// Skip if connection is disabled
|
||||
if (!igny8_is_connection_enabled()) {
|
||||
return array('disabled' => true, 'reason' => 'connection_disabled');
|
||||
}
|
||||
|
||||
if (function_exists('igny8_is_module_enabled') && !igny8_is_module_enabled('sites')) {
|
||||
return array('disabled' => true);
|
||||
}
|
||||
|
||||
$settings = igny8_get_site_scan_settings($args);
|
||||
|
||||
$site_data = array(
|
||||
'site_url' => get_site_url(),
|
||||
'site_name' => get_bloginfo('name'),
|
||||
'site_description' => get_bloginfo('description'),
|
||||
'collected_at' => current_time('mysql'),
|
||||
'settings' => $settings,
|
||||
'posts' => array(),
|
||||
'taxonomies' => array(),
|
||||
'products' => array(),
|
||||
'product_categories' => array(),
|
||||
'product_attributes' => array()
|
||||
);
|
||||
|
||||
foreach ((array) $settings['post_types'] as $post_type) {
|
||||
if (!post_type_exists($post_type) || !igny8_is_post_type_enabled($post_type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$posts = igny8_fetch_wordpress_posts($post_type, $settings['per_page'], array(
|
||||
'after' => $settings['since'],
|
||||
'status' => 'publish'
|
||||
));
|
||||
|
||||
if ($posts) {
|
||||
$site_data['posts'] = array_merge($site_data['posts'], $posts);
|
||||
}
|
||||
}
|
||||
|
||||
$tracked_taxonomies = array('category', 'post_tag', 'igny8_sectors', 'igny8_clusters');
|
||||
|
||||
// Get enabled taxonomies from settings
|
||||
if (function_exists('igny8_get_enabled_taxonomies')) {
|
||||
$enabled_taxonomies = igny8_get_enabled_taxonomies();
|
||||
if (!empty($enabled_taxonomies)) {
|
||||
$tracked_taxonomies = $enabled_taxonomies;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($tracked_taxonomies as $taxonomy) {
|
||||
if (!taxonomy_exists($taxonomy)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$terms = igny8_fetch_taxonomy_terms($taxonomy, 100);
|
||||
|
||||
// Filter product attribute terms: only sync those created by IGNY8
|
||||
if (strpos($taxonomy, 'pa_') === 0) {
|
||||
$filtered_terms = array();
|
||||
foreach ($terms as $term) {
|
||||
$origin = get_term_meta($term['id'], 'igny8_origin', true);
|
||||
if ($origin === 'igny8_app') {
|
||||
$filtered_terms[] = $term;
|
||||
}
|
||||
}
|
||||
$terms = $filtered_terms;
|
||||
}
|
||||
|
||||
if ($terms) {
|
||||
$tax_obj = get_taxonomy($taxonomy);
|
||||
$site_data['taxonomies'][$taxonomy] = array(
|
||||
'taxonomy' => array(
|
||||
'name' => $taxonomy,
|
||||
'label' => $tax_obj ? $tax_obj->label : $taxonomy,
|
||||
'description' => $tax_obj->description ?? '',
|
||||
'hierarchical' => $tax_obj ? $tax_obj->hierarchical : false,
|
||||
),
|
||||
'terms' => $terms
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($settings['include_products']) && function_exists('igny8_is_woocommerce_active') && igny8_is_woocommerce_active()) {
|
||||
require_once IGNY8_BRIDGE_PLUGIN_DIR . 'data/woocommerce.php';
|
||||
|
||||
$products = igny8_fetch_woocommerce_products(100);
|
||||
if ($products) {
|
||||
$site_data['products'] = $products;
|
||||
}
|
||||
|
||||
$product_categories = igny8_fetch_product_categories(100);
|
||||
if ($product_categories) {
|
||||
$site_data['product_categories'] = $product_categories;
|
||||
}
|
||||
|
||||
$product_attributes = igny8_fetch_product_attributes();
|
||||
if ($product_attributes) {
|
||||
$site_data['product_attributes'] = $product_attributes;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract link graph if Linker module is enabled
|
||||
if (function_exists('igny8_is_module_enabled') && igny8_is_module_enabled('linker')) {
|
||||
$post_ids = wp_list_pluck($site_data['posts'], 'id');
|
||||
$link_graph = igny8_extract_link_graph($post_ids);
|
||||
|
||||
if (!empty($link_graph)) {
|
||||
$site_data['link_graph'] = $link_graph;
|
||||
}
|
||||
}
|
||||
|
||||
$site_data['summary'] = array(
|
||||
'posts' => count($site_data['posts']),
|
||||
'taxonomies' => count($site_data['taxonomies']),
|
||||
'products' => count($site_data['products']),
|
||||
'links' => isset($site_data['link_graph']) ? count($site_data['link_graph']) : 0
|
||||
);
|
||||
|
||||
update_option('igny8_last_site_snapshot', array(
|
||||
'timestamp' => current_time('timestamp'),
|
||||
'summary' => $site_data['summary']
|
||||
));
|
||||
|
||||
return $site_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send WordPress site data to IGNY8 for semantic strategy mapping
|
||||
*
|
||||
* @param int $site_id IGNY8 site ID
|
||||
* @return array|false Response data or false on failure
|
||||
*/
|
||||
function igny8_send_site_data_to_igny8($site_id, $site_data = null, $args = array()) {
|
||||
// Skip if connection is disabled
|
||||
if (!igny8_is_connection_enabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$api = new Igny8API();
|
||||
|
||||
if (!$api->is_authenticated()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Collect all site data if not provided
|
||||
if (empty($site_data)) {
|
||||
$site_data = igny8_collect_site_data($args);
|
||||
}
|
||||
|
||||
if (empty($site_data) || isset($site_data['disabled'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Send to IGNY8 API
|
||||
$response = $api->post("/system/sites/{$site_id}/import/", array(
|
||||
'site_data' => $site_data,
|
||||
'import_type' => $args['mode'] ?? 'full_site_scan'
|
||||
));
|
||||
|
||||
if ($response['success']) {
|
||||
// Store import ID for tracking
|
||||
update_option('igny8_last_site_import_id', $response['data']['import_id'] ?? null);
|
||||
update_option('igny8_last_site_sync', current_time('timestamp'));
|
||||
|
||||
// Send link graph separately to Linker module if available
|
||||
if (!empty($site_data['link_graph']) && function_exists('igny8_is_module_enabled') && igny8_is_module_enabled('linker')) {
|
||||
$link_result = igny8_send_link_graph_to_igny8($site_id, $site_data['link_graph']);
|
||||
if ($link_result) {
|
||||
error_log(sprintf('IGNY8: Sent %d links to Linker module', $link_result['links_sent'] ?? 0));
|
||||
}
|
||||
}
|
||||
|
||||
return $response['data'];
|
||||
} else {
|
||||
error_log("IGNY8: Failed to send site data: " . ($response['error'] ?? 'Unknown error'));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync only changed posts/taxonomies since last sync
|
||||
*
|
||||
* @param int $site_id IGNY8 site ID
|
||||
* @return array|false Sync result or false on failure
|
||||
*/
|
||||
function igny8_sync_incremental_site_data($site_id, $settings = array()) {
|
||||
// Skip if connection is disabled
|
||||
if (!igny8_is_connection_enabled()) {
|
||||
return array('synced' => 0, 'message' => 'Connection disabled');
|
||||
}
|
||||
|
||||
$api = new Igny8API();
|
||||
|
||||
if (!$api->is_authenticated()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$settings = igny8_get_site_scan_settings(wp_parse_args($settings, array('mode' => 'incremental')));
|
||||
$since = $settings['since'] ?? intval(get_option('igny8_last_site_sync', 0));
|
||||
|
||||
$formatted_posts = array();
|
||||
|
||||
foreach ((array) $settings['post_types'] as $post_type) {
|
||||
if (!post_type_exists($post_type) || !igny8_is_post_type_enabled($post_type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$query_args = array(
|
||||
'post_type' => $post_type,
|
||||
'post_status' => array('publish', 'pending', 'draft', 'future'),
|
||||
'posts_per_page' => -1,
|
||||
'orderby' => 'modified',
|
||||
'order' => 'DESC',
|
||||
'suppress_filters' => true,
|
||||
);
|
||||
|
||||
if ($since) {
|
||||
$query_args['date_query'] = array(
|
||||
array(
|
||||
'column' => 'post_modified_gmt',
|
||||
'after' => gmdate('Y-m-d H:i:s', $since)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$posts = get_posts($query_args);
|
||||
|
||||
foreach ($posts as $post) {
|
||||
$word_count = str_word_count(strip_tags($post->post_content));
|
||||
|
||||
$formatted_posts[] = array(
|
||||
'id' => $post->ID,
|
||||
'title' => get_the_title($post),
|
||||
'content' => $post->post_content,
|
||||
'status' => $post->post_status,
|
||||
'modified' => $post->post_modified_gmt,
|
||||
'post_type' => $post->post_type,
|
||||
'url' => get_permalink($post),
|
||||
'taxonomies' => array(
|
||||
'categories' => wp_get_post_terms($post->ID, 'category', array('fields' => 'ids')),
|
||||
'tags' => wp_get_post_terms($post->ID, 'post_tag', array('fields' => 'ids')),
|
||||
),
|
||||
'meta' => array(
|
||||
'task_id' => get_post_meta($post->ID, '_igny8_task_id', true),
|
||||
'cluster_id' => get_post_meta($post->ID, '_igny8_cluster_id', true),
|
||||
'sector_id' => get_post_meta($post->ID, '_igny8_sector_id', true),
|
||||
'word_count' => $word_count,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($formatted_posts)) {
|
||||
return array('synced' => 0, 'message' => 'No changes since last sync');
|
||||
}
|
||||
|
||||
$response = $api->post("/system/sites/{$site_id}/sync/", array(
|
||||
'posts' => $formatted_posts,
|
||||
'sync_type' => 'incremental',
|
||||
'last_sync' => $since,
|
||||
'post_types' => $settings['post_types']
|
||||
));
|
||||
|
||||
if ($response['success']) {
|
||||
update_option('igny8_last_site_sync', current_time('timestamp'));
|
||||
update_option('igny8_last_incremental_site_sync', array(
|
||||
'timestamp' => current_time('timestamp'),
|
||||
'count' => count($formatted_posts)
|
||||
));
|
||||
|
||||
return array(
|
||||
'synced' => count($formatted_posts),
|
||||
'message' => 'Incremental sync completed'
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a full site scan and semantic mapping
|
||||
*
|
||||
* @param int $site_id IGNY8 site ID
|
||||
* @param array $settings Scan settings
|
||||
* @return array|false
|
||||
*/
|
||||
function igny8_perform_full_site_scan($site_id, $settings = array()) {
|
||||
$site_data = igny8_collect_site_data($settings);
|
||||
|
||||
if (empty($site_data) || isset($site_data['disabled'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$import = igny8_send_site_data_to_igny8($site_id, $site_data, array('mode' => 'full_site_scan'));
|
||||
|
||||
if (!$import) {
|
||||
return false;
|
||||
}
|
||||
|
||||
update_option('igny8_last_full_site_scan', current_time('timestamp'));
|
||||
|
||||
// Map to semantic strategy (requires Planner module)
|
||||
if (!function_exists('igny8_is_module_enabled') || igny8_is_module_enabled('planner')) {
|
||||
$map_response = igny8_map_site_to_semantic_strategy($site_id, $site_data);
|
||||
if (!empty($map_response['success'])) {
|
||||
update_option('igny8_last_semantic_map', current_time('timestamp'));
|
||||
update_option('igny8_last_semantic_map_summary', array(
|
||||
'sectors' => count($map_response['data']['sectors'] ?? array()),
|
||||
'keywords' => count($map_response['data']['keywords'] ?? array())
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Send link graph to Linker module if available
|
||||
if (!empty($site_data['link_graph']) && function_exists('igny8_is_module_enabled') && igny8_is_module_enabled('linker')) {
|
||||
$link_result = igny8_send_link_graph_to_igny8($site_id, $site_data['link_graph']);
|
||||
if ($link_result) {
|
||||
error_log(sprintf('IGNY8: Sent %d links to Linker module during full scan', $link_result['links_sent'] ?? 0));
|
||||
}
|
||||
}
|
||||
|
||||
return $import;
|
||||
}
|
||||
|
||||
@@ -1,226 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Integration
|
||||
*
|
||||
* Fetches WooCommerce products, categories, and attributes
|
||||
* Follows WORDPRESS-PLUGIN-INTEGRATION.md guidelines
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if WooCommerce is active
|
||||
*
|
||||
* @return bool True if WooCommerce is active
|
||||
*/
|
||||
function igny8_is_woocommerce_active() {
|
||||
return class_exists('WooCommerce');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all WooCommerce products
|
||||
*
|
||||
* @param int $per_page Products per page
|
||||
* @return array|false Formatted products array or false on failure
|
||||
*/
|
||||
function igny8_fetch_woocommerce_products($per_page = 100) {
|
||||
// Check if WooCommerce is active
|
||||
if (!igny8_is_woocommerce_active()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get WooCommerce API credentials
|
||||
$consumer_key = get_option('woocommerce_api_consumer_key', '');
|
||||
$consumer_secret = get_option('woocommerce_api_consumer_secret', '');
|
||||
|
||||
if (empty($consumer_key) || empty($consumer_secret)) {
|
||||
// Try to use basic auth if API keys not set
|
||||
$auth = '';
|
||||
} else {
|
||||
$auth = 'Basic ' . base64_encode($consumer_key . ':' . $consumer_secret);
|
||||
}
|
||||
|
||||
$headers = array();
|
||||
if ($auth) {
|
||||
$headers['Authorization'] = $auth;
|
||||
}
|
||||
|
||||
$wp_response = wp_remote_get(sprintf(
|
||||
'%s/wp-json/wc/v3/products?per_page=%d&status=publish',
|
||||
get_site_url(),
|
||||
$per_page
|
||||
), array(
|
||||
'headers' => $headers
|
||||
));
|
||||
|
||||
if (is_wp_error($wp_response)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$products = json_decode(wp_remote_retrieve_body($wp_response), true);
|
||||
|
||||
if (!is_array($products)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$formatted_products = array();
|
||||
foreach ($products as $product) {
|
||||
$formatted_products[] = array(
|
||||
'id' => $product['id'],
|
||||
'name' => $product['name'],
|
||||
'slug' => $product['slug'],
|
||||
'sku' => $product['sku'],
|
||||
'type' => $product['type'],
|
||||
'status' => $product['status'],
|
||||
'description' => $product['description'],
|
||||
'short_description' => $product['short_description'],
|
||||
'price' => $product['price'],
|
||||
'regular_price' => $product['regular_price'],
|
||||
'sale_price' => $product['sale_price'],
|
||||
'on_sale' => $product['on_sale'],
|
||||
'stock_status' => $product['stock_status'],
|
||||
'stock_quantity' => $product['stock_quantity'],
|
||||
'categories' => $product['categories'] ?? array(),
|
||||
'tags' => $product['tags'] ?? array(),
|
||||
'images' => $product['images'] ?? array(),
|
||||
'attributes' => $product['attributes'] ?? array(),
|
||||
'variations' => $product['variations'] ?? array(),
|
||||
'url' => $product['permalink']
|
||||
);
|
||||
}
|
||||
|
||||
return $formatted_products;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch WooCommerce product categories
|
||||
*
|
||||
* @param int $per_page Categories per page
|
||||
* @return array|false Formatted categories array or false on failure
|
||||
*/
|
||||
function igny8_fetch_product_categories($per_page = 100) {
|
||||
if (!igny8_is_woocommerce_active()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$consumer_key = get_option('woocommerce_api_consumer_key', '');
|
||||
$consumer_secret = get_option('woocommerce_api_consumer_secret', '');
|
||||
|
||||
$headers = array();
|
||||
if ($consumer_key && $consumer_secret) {
|
||||
$headers['Authorization'] = 'Basic ' . base64_encode($consumer_key . ':' . $consumer_secret);
|
||||
}
|
||||
|
||||
$wp_response = wp_remote_get(sprintf(
|
||||
'%s/wp-json/wc/v3/products/categories?per_page=%d',
|
||||
get_site_url(),
|
||||
$per_page
|
||||
), array(
|
||||
'headers' => $headers
|
||||
));
|
||||
|
||||
if (is_wp_error($wp_response)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$categories = json_decode(wp_remote_retrieve_body($wp_response), true);
|
||||
|
||||
if (!is_array($categories)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$formatted_categories = array();
|
||||
foreach ($categories as $category) {
|
||||
$formatted_categories[] = array(
|
||||
'id' => $category['id'],
|
||||
'name' => $category['name'],
|
||||
'slug' => $category['slug'],
|
||||
'description' => $category['description'] ?? '',
|
||||
'count' => $category['count'],
|
||||
'parent' => $category['parent'] ?? 0,
|
||||
'image' => $category['image']['src'] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
return $formatted_categories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch WooCommerce product attributes
|
||||
*
|
||||
* @return array|false Formatted attributes array or false on failure
|
||||
*/
|
||||
function igny8_fetch_product_attributes() {
|
||||
if (!igny8_is_woocommerce_active()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$consumer_key = get_option('woocommerce_api_consumer_key', '');
|
||||
$consumer_secret = get_option('woocommerce_api_consumer_secret', '');
|
||||
|
||||
$headers = array();
|
||||
if ($consumer_key && $consumer_secret) {
|
||||
$headers['Authorization'] = 'Basic ' . base64_encode($consumer_key . ':' . $consumer_secret);
|
||||
}
|
||||
|
||||
$wp_response = wp_remote_get(
|
||||
get_site_url() . '/wp-json/wc/v3/products/attributes',
|
||||
array(
|
||||
'headers' => $headers
|
||||
)
|
||||
);
|
||||
|
||||
if (is_wp_error($wp_response)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$attributes = json_decode(wp_remote_retrieve_body($wp_response), true);
|
||||
|
||||
if (!is_array($attributes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$formatted_attributes = array();
|
||||
foreach ($attributes as $attribute) {
|
||||
// Get attribute terms
|
||||
$terms_response = wp_remote_get(sprintf(
|
||||
'%s/wp-json/wc/v3/products/attributes/%d/terms',
|
||||
get_site_url(),
|
||||
$attribute['id']
|
||||
), array(
|
||||
'headers' => $headers
|
||||
));
|
||||
|
||||
$terms = array();
|
||||
if (!is_wp_error($terms_response)) {
|
||||
$terms_data = json_decode(wp_remote_retrieve_body($terms_response), true);
|
||||
if (is_array($terms_data)) {
|
||||
foreach ($terms_data as $term) {
|
||||
$terms[] = array(
|
||||
'id' => $term['id'],
|
||||
'name' => $term['name'],
|
||||
'slug' => $term['slug']
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$formatted_attributes[] = array(
|
||||
'id' => $attribute['id'],
|
||||
'name' => $attribute['name'],
|
||||
'slug' => $attribute['slug'],
|
||||
'type' => $attribute['type'],
|
||||
'order_by' => $attribute['order_by'],
|
||||
'has_archives' => $attribute['has_archives'],
|
||||
'terms' => $terms
|
||||
);
|
||||
}
|
||||
|
||||
return $formatted_attributes;
|
||||
}
|
||||
|
||||
@@ -1,650 +0,0 @@
|
||||
# Actionable Implementation Plan - WordPress Publishing Fix
|
||||
|
||||
**Date:** November 29, 2025
|
||||
**Issue:** Only title is being published to WordPress, no content_html or other fields
|
||||
**Root Cause:** Data mismatch between IGNY8 backend payload and WordPress plugin expectations
|
||||
|
||||
---
|
||||
|
||||
## 🔴 CRITICAL ISSUE DIAGNOSED
|
||||
|
||||
### The Problem
|
||||
|
||||
**Current Behavior:**
|
||||
- IGNY8 backend sends `content_html` field in payload
|
||||
- WordPress plugin receives the data BUT the POST request payload does NOT include the actual content data from the `ContentPost` model
|
||||
- Only `title` appears in WordPress because the REST API endpoint fetches data from the wrong endpoint
|
||||
|
||||
**Root Cause Analysis:**
|
||||
|
||||
1. **File:** `igny8_core/tasks/wordpress_publishing.py` (Line 53-75)
|
||||
```python
|
||||
content_data = {
|
||||
'content_id': content.id,
|
||||
'task_id': task_id,
|
||||
'title': content.title,
|
||||
'content_html': content.content_html or content.content, # ← Should work
|
||||
'excerpt': content.brief or '', # ← Field name mismatch
|
||||
'status': 'publish',
|
||||
# ... more fields
|
||||
}
|
||||
```
|
||||
|
||||
2. **File:** `includes/class-igny8-rest-api.php` (Line 507-525)
|
||||
```php
|
||||
// Try to get content by different endpoints
|
||||
$content_data = null;
|
||||
|
||||
if ($task_id) {
|
||||
$response = $api->get("/writer/tasks/{$task_id}/"); // ← WRONG!
|
||||
if ($response['success']) {
|
||||
$content_data = $response['data'];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**The Issue:** WordPress is fetching from `/writer/tasks/{task_id}/` which returns `Tasks` model data, NOT `Content` model data!
|
||||
|
||||
3. **Model Mismatch:**
|
||||
- `Tasks` model has: `title`, `description`, `keywords`, `word_count`, `status`
|
||||
- `Content` model has: `title`, `content_html`, `meta_title`, `meta_description`
|
||||
- WordPress gets `Tasks` data which has NO `content_html` field!
|
||||
|
||||
---
|
||||
|
||||
## ✅ SOLUTION ARCHITECTURE
|
||||
|
||||
### Phase 1: Fix Data Flow (CRITICAL - Do First)
|
||||
|
||||
#### Problem 1.1: WordPress REST Endpoint Fetches Wrong Data
|
||||
|
||||
**File to Fix:** `c:\Users\Hp\vscode\igny8-wp-integration\includes\class-igny8-rest-api.php`
|
||||
|
||||
**Current Code (Line 507-525):**
|
||||
```php
|
||||
// Try to get content by different endpoints
|
||||
$content_data = null;
|
||||
|
||||
if ($task_id) {
|
||||
$response = $api->get("/writer/tasks/{$task_id}/"); // ← FETCHES TASKS MODEL
|
||||
if ($response['success']) {
|
||||
$content_data = $response['data'];
|
||||
}
|
||||
}
|
||||
|
||||
if (!$content_data && $content_id) {
|
||||
// Try content endpoint if available
|
||||
$response = $api->get("/content/{$content_id}/"); // ← THIS IS CORRECT
|
||||
if ($response['success']) {
|
||||
$content_data = $response['data'];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fix Required:**
|
||||
```php
|
||||
// REMOVE the task endpoint fetch entirely
|
||||
// WordPress should ONLY use data sent in POST body from IGNY8
|
||||
|
||||
public function publish_content_to_wordpress($request) {
|
||||
// ... existing validation ...
|
||||
|
||||
// Get all data from POST body (IGNY8 already sent everything)
|
||||
$content_data = $request->get_json_params();
|
||||
|
||||
// Validate required fields
|
||||
if (empty($content_data['title']) || empty($content_data['content_html'])) {
|
||||
return $this->build_unified_response(
|
||||
false,
|
||||
null,
|
||||
'Missing required fields: title and content_html',
|
||||
'missing_fields',
|
||||
null,
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
// NO API CALL BACK TO IGNY8 - just use the data we received!
|
||||
// ... proceed to create post ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Problem 1.2: IGNY8 Backend Field Name Mismatch
|
||||
|
||||
**File to Check:** `e:\Projects\...\igny8\backend\igny8_core\business\content\models.py`
|
||||
|
||||
**Content Model Fields (Lines 166-173):**
|
||||
```python
|
||||
# Core content fields
|
||||
title = models.CharField(max_length=255, db_index=True)
|
||||
content_html = models.TextField(help_text="Final HTML content") # ✓ CORRECT
|
||||
word_count = models.IntegerField(default=0)
|
||||
|
||||
# SEO fields
|
||||
meta_title = models.CharField(max_length=255, blank=True, null=True)
|
||||
meta_description = models.TextField(blank=True, null=True)
|
||||
primary_keyword = models.CharField(max_length=255, blank=True, null=True)
|
||||
```
|
||||
|
||||
**File to Fix:** `e:\Projects\...\igny8\backend\igny8_core\tasks\wordpress_publishing.py`
|
||||
|
||||
**Current Code (Lines 53-75):**
|
||||
```python
|
||||
content_data = {
|
||||
'content_id': content.id,
|
||||
'task_id': task_id,
|
||||
'title': content.title,
|
||||
'content_html': content.content_html or content.content, # ✓ CORRECT
|
||||
'excerpt': content.brief or '', # ← WRONG! Content model has no 'brief' field
|
||||
'status': 'publish',
|
||||
'author_email': content.author.email if content.author else None,
|
||||
'author_name': content.author.get_full_name() if content.author else None,
|
||||
'published_at': content.published_at.isoformat() if content.published_at else None,
|
||||
'seo_title': getattr(content, 'seo_title', ''), # ← WRONG! Should be 'meta_title'
|
||||
'seo_description': getattr(content, 'seo_description', ''), # ← WRONG! Should be 'meta_description'
|
||||
'featured_image_url': content.featured_image.url if content.featured_image else None,
|
||||
'sectors': [{'id': s.id, 'name': s.name} for s in content.sectors.all()],
|
||||
'clusters': [{'id': c.id, 'name': c.name} for c in content.clusters.all()],
|
||||
'tags': getattr(content, 'tags', []), # ← Needs verification
|
||||
'focus_keywords': getattr(content, 'focus_keywords', []) # ← Should be 'secondary_keywords'
|
||||
}
|
||||
```
|
||||
|
||||
**Fix Required:**
|
||||
```python
|
||||
# Generate excerpt from content_html if not present
|
||||
excerpt = ''
|
||||
if content.content_html:
|
||||
# Strip HTML and get first 155 characters
|
||||
from html import unescape
|
||||
import re
|
||||
text = re.sub('<[^<]+?>', '', content.content_html)
|
||||
text = unescape(text).strip()
|
||||
excerpt = text[:155] + '...' if len(text) > 155 else text
|
||||
|
||||
content_data = {
|
||||
'content_id': content.id,
|
||||
'task_id': task_id,
|
||||
'title': content.title,
|
||||
'content_html': content.content_html, # ✓ REQUIRED
|
||||
'excerpt': excerpt, # Generated from content
|
||||
'status': 'publish',
|
||||
'author_email': content.author.email if content.author else None,
|
||||
'author_name': content.author.get_full_name() if content.author else None,
|
||||
'published_at': content.published_at.isoformat() if content.published_at else None,
|
||||
|
||||
# SEO Fields (correct field names)
|
||||
'seo_title': content.meta_title or '',
|
||||
'seo_description': content.meta_description or '',
|
||||
'primary_keyword': content.primary_keyword or '',
|
||||
'secondary_keywords': content.secondary_keywords or [],
|
||||
|
||||
# Media
|
||||
'featured_image_url': content.featured_image.url if content.featured_image else None,
|
||||
|
||||
# Relationships (need to verify these exist on Content model)
|
||||
'cluster_id': content.cluster.id if content.cluster else None,
|
||||
'cluster_name': content.cluster.name if content.cluster else None,
|
||||
'sector_id': content.sector.id if content.sector else None,
|
||||
'sector_name': content.sector.name if content.sector else None,
|
||||
|
||||
# Content classification
|
||||
'content_type': content.content_type,
|
||||
'content_structure': content.content_structure,
|
||||
|
||||
# Categories/Tags (if they exist as relations)
|
||||
'categories': [], # TODO: Add if Content model has category relation
|
||||
'tags': [], # TODO: Add if Content model has tag relation
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Verify Content Model Relations
|
||||
|
||||
**Action Required:** Check if `Content` model has these fields/relations:
|
||||
|
||||
```python
|
||||
# Need to verify in Content model:
|
||||
- author (ForeignKey to User)
|
||||
- published_at (DateTimeField)
|
||||
- featured_image (FileField/ImageField)
|
||||
- cluster (ForeignKey) ✓ CONFIRMED
|
||||
- sector (ForeignKey) ✓ CONFIRMED from SiteSectorBaseModel
|
||||
- categories (ManyToMany?)
|
||||
- tags (ManyToMany?)
|
||||
```
|
||||
|
||||
**File to Check:** `e:\Projects\...\igny8\backend\igny8_core\business\content\models.py` (continue reading from line 200)
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: WordPress Plugin - Remove API Callback
|
||||
|
||||
**File:** `c:\Users\Hp\vscode\igny8-wp-integration\includes\class-igny8-rest-api.php`
|
||||
|
||||
**Lines to REMOVE:** 507-545
|
||||
|
||||
**Replacement Logic:**
|
||||
|
||||
```php
|
||||
public function publish_content_to_wordpress($request) {
|
||||
// 1. Check connection
|
||||
if (!igny8_is_connection_enabled()) {
|
||||
return $this->build_unified_response(false, null, 'Connection disabled', 'connection_disabled', null, 403);
|
||||
}
|
||||
|
||||
// 2. Get ALL data from POST body (IGNY8 sends everything)
|
||||
$content_data = $request->get_json_params();
|
||||
|
||||
// 3. Validate required fields
|
||||
if (empty($content_data['content_id'])) {
|
||||
return $this->build_unified_response(false, null, 'Missing content_id', 'missing_content_id', null, 400);
|
||||
}
|
||||
|
||||
if (empty($content_data['title'])) {
|
||||
return $this->build_unified_response(false, null, 'Missing title', 'missing_title', null, 400);
|
||||
}
|
||||
|
||||
if (empty($content_data['content_html'])) {
|
||||
return $this->build_unified_response(false, null, 'Missing content_html', 'missing_content_html', null, 400);
|
||||
}
|
||||
|
||||
// 4. Check if content already exists
|
||||
$existing_posts = get_posts(array(
|
||||
'meta_key' => '_igny8_content_id',
|
||||
'meta_value' => $content_data['content_id'],
|
||||
'post_type' => 'any',
|
||||
'posts_per_page' => 1
|
||||
));
|
||||
|
||||
if (!empty($existing_posts)) {
|
||||
return $this->build_unified_response(
|
||||
false,
|
||||
array('post_id' => $existing_posts[0]->ID),
|
||||
'Content already exists',
|
||||
'content_exists',
|
||||
null,
|
||||
409
|
||||
);
|
||||
}
|
||||
|
||||
// 5. Create WordPress post (function expects content_data with content_html)
|
||||
$post_id = igny8_create_wordpress_post_from_task($content_data);
|
||||
|
||||
if (is_wp_error($post_id)) {
|
||||
return $this->build_unified_response(
|
||||
false,
|
||||
null,
|
||||
'Failed to create post: ' . $post_id->get_error_message(),
|
||||
'post_creation_failed',
|
||||
null,
|
||||
500
|
||||
);
|
||||
}
|
||||
|
||||
// 6. Return success
|
||||
return $this->build_unified_response(
|
||||
true,
|
||||
array(
|
||||
'post_id' => $post_id,
|
||||
'post_url' => get_permalink($post_id),
|
||||
'post_status' => get_post_status($post_id),
|
||||
'content_id' => $content_data['content_id'],
|
||||
'task_id' => $content_data['task_id'] ?? null
|
||||
),
|
||||
'Content successfully published to WordPress',
|
||||
null,
|
||||
null,
|
||||
201
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Add Logging for Debugging
|
||||
|
||||
**File:** `e:\Projects\...\igny8\backend\igny8_core\tasks\wordpress_publishing.py`
|
||||
|
||||
**Add after line 75:**
|
||||
|
||||
```python
|
||||
# Log the payload being sent
|
||||
logger.info(f"Publishing content {content_id} to WordPress")
|
||||
logger.debug(f"Payload: {json.dumps(content_data, indent=2)}")
|
||||
|
||||
response = requests.post(
|
||||
wordpress_url,
|
||||
json=content_data,
|
||||
headers=headers,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
# Log response
|
||||
logger.info(f"WordPress response status: {response.status_code}")
|
||||
logger.debug(f"WordPress response body: {response.text}")
|
||||
```
|
||||
|
||||
**File:** `c:\Users\Hp\vscode\igny8-wp-integration\includes\class-igny8-rest-api.php`
|
||||
|
||||
**Add at start of publish_content_to_wordpress():**
|
||||
|
||||
```php
|
||||
// Debug log incoming data
|
||||
error_log('IGNY8 Publish Request - Content ID: ' . ($content_data['content_id'] ?? 'MISSING'));
|
||||
error_log('IGNY8 Publish Request - Has title: ' . (empty($content_data['title']) ? 'NO' : 'YES'));
|
||||
error_log('IGNY8 Publish Request - Has content_html: ' . (empty($content_data['content_html']) ? 'NO' : 'YES'));
|
||||
error_log('IGNY8 Publish Request - Content HTML length: ' . strlen($content_data['content_html'] ?? ''));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 STEP-BY-STEP IMPLEMENTATION CHECKLIST
|
||||
|
||||
### ✅ Step 1: Fix IGNY8 Backend Payload (HIGHEST PRIORITY)
|
||||
|
||||
**File:** `igny8_core/tasks/wordpress_publishing.py`
|
||||
|
||||
- [ ] Line 53-75: Update field names to match `Content` model
|
||||
- [ ] Change `seo_title` → `meta_title`
|
||||
- [ ] Change `seo_description` → `meta_description`
|
||||
- [ ] Remove `brief` (doesn't exist on Content model)
|
||||
- [ ] Generate `excerpt` from `content_html`
|
||||
- [ ] Change `focus_keywords` → `secondary_keywords`
|
||||
- [ ] Add `primary_keyword` field
|
||||
- [ ] Verify `author`, `published_at`, `featured_image` fields exist
|
||||
- [ ] Add `content_type` and `content_structure` fields
|
||||
- [ ] Add `cluster_id` and `sector_id` properly
|
||||
|
||||
- [ ] Add comprehensive logging
|
||||
- [ ] Log payload before sending
|
||||
- [ ] Log HTTP response status and body
|
||||
- [ ] Log success/failure with details
|
||||
|
||||
**Expected Result:** Payload contains actual `content_html` with full HTML content
|
||||
|
||||
---
|
||||
|
||||
### ✅ Step 2: Fix WordPress Plugin REST Endpoint
|
||||
|
||||
**File:** `includes/class-igny8-rest-api.php`
|
||||
|
||||
- [ ] Line 507-545: REMOVE API callback to IGNY8
|
||||
- [ ] Delete `$api->get("/writer/tasks/{$task_id}/")`
|
||||
- [ ] Delete `$api->get("/content/{$content_id}/")`
|
||||
- [ ] Use `$request->get_json_params()` directly
|
||||
|
||||
- [ ] Add proper validation
|
||||
- [ ] Validate `content_id` exists
|
||||
- [ ] Validate `title` exists
|
||||
- [ ] Validate `content_html` exists and is not empty
|
||||
- [ ] Validate `content_html` length > 100 characters
|
||||
|
||||
- [ ] Add comprehensive logging
|
||||
- [ ] Log received content_id
|
||||
- [ ] Log if title present
|
||||
- [ ] Log if content_html present
|
||||
- [ ] Log content_html length
|
||||
|
||||
**Expected Result:** WordPress uses data from POST body, not API callback
|
||||
|
||||
---
|
||||
|
||||
### ✅ Step 3: Verify WordPress Post Creation Function
|
||||
|
||||
**File:** `sync/igny8-to-wp.php`
|
||||
|
||||
- [ ] Function `igny8_create_wordpress_post_from_task()` Line 69-285
|
||||
- [ ] Verify it expects `content_html` field (Line 88)
|
||||
- [ ] Verify it uses `wp_kses_post($content_html)` (Line 101)
|
||||
- [ ] Verify `post_content` is set correctly (Line 101)
|
||||
- [ ] Verify SEO meta fields mapped correctly
|
||||
- [ ] `meta_title` → multiple SEO plugins
|
||||
- [ ] `meta_description` → multiple SEO plugins
|
||||
- [ ] Verify all IGNY8 meta fields stored
|
||||
- [ ] `_igny8_task_id`
|
||||
- [ ] `_igny8_content_id`
|
||||
- [ ] `_igny8_cluster_id`
|
||||
- [ ] `_igny8_sector_id`
|
||||
- [ ] `_igny8_content_type`
|
||||
- [ ] `_igny8_content_structure`
|
||||
|
||||
**Expected Result:** Full content published with all metadata
|
||||
|
||||
---
|
||||
|
||||
### ✅ Step 4: Test End-to-End Flow
|
||||
|
||||
**Manual Test Steps:**
|
||||
|
||||
1. **IGNY8 Backend - Trigger Publish:**
|
||||
```python
|
||||
# In Django shell or admin
|
||||
from igny8_core.models import Content, SiteIntegration
|
||||
from igny8_core.tasks.wordpress_publishing import publish_content_to_wordpress
|
||||
|
||||
content = Content.objects.first() # Get a content with content_html
|
||||
site_integration = SiteIntegration.objects.first()
|
||||
|
||||
# Check content has data
|
||||
print(f"Title: {content.title}")
|
||||
print(f"Content HTML length: {len(content.content_html)}")
|
||||
print(f"Meta Title: {content.meta_title}")
|
||||
|
||||
# Trigger publish
|
||||
result = publish_content_to_wordpress(content.id, site_integration.id)
|
||||
print(result)
|
||||
```
|
||||
|
||||
2. **Check Logs:**
|
||||
- IGNY8 backend logs: Should show full payload with `content_html`
|
||||
- WordPress logs: Should show received data with `content_html`
|
||||
|
||||
3. **Verify WordPress Post:**
|
||||
```php
|
||||
// In WordPress admin or WP-CLI
|
||||
$post = get_post($post_id);
|
||||
echo "Title: " . $post->post_title . "\n";
|
||||
echo "Content length: " . strlen($post->post_content) . "\n";
|
||||
echo "Content preview: " . substr($post->post_content, 0, 200) . "\n";
|
||||
|
||||
// Check meta
|
||||
echo "Task ID: " . get_post_meta($post_id, '_igny8_task_id', true) . "\n";
|
||||
echo "Content ID: " . get_post_meta($post_id, '_igny8_content_id', true) . "\n";
|
||||
echo "Cluster ID: " . get_post_meta($post_id, '_igny8_cluster_id', true) . "\n";
|
||||
```
|
||||
|
||||
**Expected Result:** Post has full HTML content, all metadata present
|
||||
|
||||
---
|
||||
|
||||
## 🔍 DEBUGGING CHECKLIST
|
||||
|
||||
If content still not publishing:
|
||||
|
||||
### Debug Point 1: IGNY8 Payload
|
||||
```python
|
||||
# Add to wordpress_publishing.py after line 75
|
||||
print("=" * 50)
|
||||
print("CONTENT DATA BEING SENT:")
|
||||
print(f"content_id: {content_data.get('content_id')}")
|
||||
print(f"title: {content_data.get('title')}")
|
||||
print(f"content_html length: {len(content_data.get('content_html', ''))}")
|
||||
print(f"content_html preview: {content_data.get('content_html', '')[:200]}")
|
||||
print("=" * 50)
|
||||
```
|
||||
|
||||
### Debug Point 2: HTTP Request
|
||||
```python
|
||||
# Add after response = requests.post(...)
|
||||
print(f"HTTP Status: {response.status_code}")
|
||||
print(f"Response: {response.text[:500]}")
|
||||
```
|
||||
|
||||
### Debug Point 3: WordPress Reception
|
||||
```php
|
||||
// Add to publish_content_to_wordpress() at line 1
|
||||
$raw_body = $request->get_body();
|
||||
error_log("IGNY8 Raw Request Body: " . substr($raw_body, 0, 500));
|
||||
|
||||
$content_data = $request->get_json_params();
|
||||
error_log("IGNY8 Parsed Data Keys: " . implode(', ', array_keys($content_data)));
|
||||
error_log("IGNY8 Content HTML Length: " . strlen($content_data['content_html'] ?? ''));
|
||||
```
|
||||
|
||||
### Debug Point 4: Post Creation
|
||||
```php
|
||||
// Add to igny8_create_wordpress_post_from_task() after line 100
|
||||
error_log("Creating post with title: " . $post_data['post_title']);
|
||||
error_log("Post content length: " . strlen($post_data['post_content']));
|
||||
error_log("Post content preview: " . substr($post_data['post_content'], 0, 200));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚨 COMMON PITFALLS TO AVOID
|
||||
|
||||
1. **DO NOT fetch data from `/writer/tasks/` endpoint** - Tasks model ≠ Content model
|
||||
2. **DO NOT assume field names** - Verify against actual model definition
|
||||
3. **DO NOT skip validation** - Empty `content_html` will create empty posts
|
||||
4. **DO NOT ignore errors** - Log everything for debugging
|
||||
5. **DO NOT mix up Content vs Tasks** - They are separate models with different fields
|
||||
|
||||
---
|
||||
|
||||
## 📊 DATA FLOW VALIDATION
|
||||
|
||||
### Correct Flow:
|
||||
```
|
||||
Content Model (DB)
|
||||
↓ ORM fetch
|
||||
content.content_html = "<p>Full HTML content...</p>"
|
||||
↓ Prepare payload
|
||||
content_data['content_html'] = "<p>Full HTML content...</p>"
|
||||
↓ JSON serialize
|
||||
{"content_html": "<p>Full HTML content...</p>"}
|
||||
↓ HTTP POST
|
||||
WordPress receives: content_html in POST body
|
||||
↓ Parse JSON
|
||||
$content_data['content_html'] = "<p>Full HTML content...</p>"
|
||||
↓ Create post
|
||||
wp_insert_post(['post_content' => wp_kses_post($content_html)])
|
||||
↓ Database insert
|
||||
wp_posts.post_content = "<p>Full HTML content...</p>"
|
||||
```
|
||||
|
||||
### Current Broken Flow:
|
||||
```
|
||||
Content Model (DB)
|
||||
↓ ORM fetch
|
||||
content.content_html = "<p>Full HTML content...</p>"
|
||||
↓ Prepare payload
|
||||
content_data['content_html'] = "<p>Full HTML content...</p>"
|
||||
↓ JSON serialize & HTTP POST
|
||||
WordPress receives: content_html in POST body
|
||||
↓ IGNORES POST BODY!
|
||||
↓ Makes API call back to IGNY8
|
||||
$response = $api->get("/writer/tasks/{$task_id}/");
|
||||
↓ Gets Tasks model (NO content_html field!)
|
||||
$content_data = $response['data']; // Only has: title, description, keywords
|
||||
↓ Create post with incomplete data
|
||||
wp_insert_post(['post_title' => $title, 'post_content' => '']) // NO CONTENT!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ SUCCESS CRITERIA
|
||||
|
||||
After implementation, verify:
|
||||
|
||||
1. **IGNY8 Backend:**
|
||||
- [ ] Payload contains `content_html` field
|
||||
- [ ] `content_html` has actual HTML content (length > 100)
|
||||
- [ ] All SEO fields present (`meta_title`, `meta_description`, `primary_keyword`)
|
||||
- [ ] Relationships present (`cluster_id`, `sector_id`)
|
||||
- [ ] Logs show full payload being sent
|
||||
|
||||
2. **WordPress Plugin:**
|
||||
- [ ] Receives `content_html` in POST body
|
||||
- [ ] Does NOT make API callback to IGNY8
|
||||
- [ ] Creates post with `post_content` = `content_html`
|
||||
- [ ] Stores all meta fields correctly
|
||||
- [ ] Returns success response with post_id and post_url
|
||||
|
||||
3. **WordPress Post:**
|
||||
- [ ] Has title
|
||||
- [ ] Has full HTML content (not empty)
|
||||
- [ ] Has excerpt
|
||||
- [ ] Has SEO meta (if SEO plugin active)
|
||||
- [ ] Has IGNY8 meta fields (content_id, task_id, cluster_id, etc.)
|
||||
- [ ] Has correct post_status
|
||||
- [ ] Has correct post_type
|
||||
|
||||
4. **End-to-End:**
|
||||
- [ ] IGNY8 → WordPress: Content publishes successfully
|
||||
- [ ] WordPress post viewable and formatted correctly
|
||||
- [ ] IGNY8 backend updated with wordpress_post_id and wordpress_post_url
|
||||
- [ ] No errors in logs
|
||||
|
||||
---
|
||||
|
||||
## 📝 IMPLEMENTATION ORDER (Priority)
|
||||
|
||||
### Day 1: Critical Fixes
|
||||
1. Fix IGNY8 backend payload field names (1-2 hours)
|
||||
2. Add logging to IGNY8 backend (30 minutes)
|
||||
3. Fix WordPress plugin - remove API callback (1 hour)
|
||||
4. Add logging to WordPress plugin (30 minutes)
|
||||
5. Test with one piece of content (1 hour)
|
||||
|
||||
### Day 2: Verification & Polish
|
||||
6. Verify all Content model fields are sent (2 hours)
|
||||
7. Test with 10 different content pieces (1 hour)
|
||||
8. Fix any edge cases discovered (2 hours)
|
||||
9. Document the changes (1 hour)
|
||||
|
||||
### Day 3: Additional Features (From Plan)
|
||||
10. Implement atomic transactions (Phase 1.1 from plan)
|
||||
11. Add pre-flight validation (Phase 1.2 from plan)
|
||||
12. Implement duplicate prevention (Phase 1.3 from plan)
|
||||
13. Add post-publish verification (Phase 1.4 from plan)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 FINAL VALIDATION TEST
|
||||
|
||||
Run this test after all fixes:
|
||||
|
||||
```python
|
||||
# IGNY8 Backend Test
|
||||
from igny8_core.models import Content
|
||||
from igny8_core.tasks.wordpress_publishing import publish_content_to_wordpress
|
||||
|
||||
# Create test content
|
||||
content = Content.objects.create(
|
||||
site_id=1,
|
||||
sector_id=1,
|
||||
cluster_id=1,
|
||||
title="Test Post - " + str(timezone.now()),
|
||||
content_html="<h1>Test Header</h1><p>This is test content with <strong>bold</strong> text.</p>",
|
||||
meta_title="Test SEO Title",
|
||||
meta_description="Test SEO description for testing",
|
||||
content_type='post',
|
||||
content_structure='article',
|
||||
)
|
||||
|
||||
# Publish
|
||||
result = publish_content_to_wordpress.delay(content.id, 1)
|
||||
print(f"Result: {result.get()}")
|
||||
|
||||
# Check WordPress
|
||||
# Go to WordPress admin → Posts → Should see new post with full HTML content
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**This plan is based on ACTUAL codebase analysis, not assumptions.**
|
||||
**Follow this step-by-step to fix the publishing issue.**
|
||||
@@ -1,114 +0,0 @@
|
||||
# Authentication System Audit - IGNY8 WordPress Plugin
|
||||
|
||||
**Date**: 2025-01-XX
|
||||
**Status**: ✅ Fixed
|
||||
|
||||
## Issue Summary
|
||||
|
||||
The WordPress plugin was showing "Failed to connect to IGNY8 API: Not authenticated" error when attempting to connect, even when valid Site ID and API Key were provided.
|
||||
|
||||
## Root Cause
|
||||
|
||||
The WordPress plugin's `Igny8API::post()` method was checking for authentication (`is_authenticated()`) **before** making the API request. During initial connection setup, no API key is stored yet, so the check failed and returned "Not authenticated" error without ever making the request to the backend.
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
### Expected Flow
|
||||
1. User enters Site ID and API Key in WordPress plugin settings
|
||||
2. Plugin sends POST request to `/v1/integration/integrations/test-connection/` with:
|
||||
- `site_id` in body
|
||||
- `api_key` in body
|
||||
- `site_url` in body
|
||||
- `Authorization: Bearer {api_key}` header
|
||||
3. Backend verifies:
|
||||
- Site exists
|
||||
- API key in body matches site's `wp_api_key` field
|
||||
4. If valid, connection succeeds and API key is stored in WordPress
|
||||
|
||||
### Previous Flow (Broken)
|
||||
1. User enters Site ID and API Key
|
||||
2. Plugin creates `Igny8API` instance (no API key stored yet)
|
||||
3. Plugin calls `$api->post()` which checks `is_authenticated()`
|
||||
4. Check fails → returns "Not authenticated" error immediately
|
||||
5. Request never reaches backend
|
||||
|
||||
## Fixes Applied
|
||||
|
||||
### 1. WordPress Plugin - API Class (`includes/class-igny8-api.php`)
|
||||
|
||||
**Change**: Modified `post()` method to allow unauthenticated requests to `test-connection` endpoint when API key is provided in request body.
|
||||
|
||||
```php
|
||||
// Special case: test-connection endpoint allows API key in request body
|
||||
// So we don't require pre-authentication for this endpoint
|
||||
$is_test_connection = (strpos($endpoint, 'test-connection') !== false);
|
||||
$has_api_key_in_data = !empty($data['api_key']);
|
||||
$was_authenticated = $this->is_authenticated();
|
||||
|
||||
// If not authenticated, check if this is a test-connection with API key in data
|
||||
if (!$was_authenticated) {
|
||||
if ($is_test_connection && $has_api_key_in_data) {
|
||||
// Temporarily set the API key for this request
|
||||
$temp_api_key = $this->access_token;
|
||||
$this->access_token = $data['api_key'];
|
||||
} else {
|
||||
return array('success' => false, 'error' => 'Not authenticated', 'http_status' => 401);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Result**: Plugin can now make test-connection requests even without pre-stored API key.
|
||||
|
||||
### 2. WordPress Plugin - Admin Class (`admin/class-admin.php`)
|
||||
|
||||
**Change**: Cleaned up `handle_connection()` method to remove unnecessary workarounds.
|
||||
|
||||
**Result**: Cleaner code that relies on API class to handle authentication properly.
|
||||
|
||||
### 3. Backend - Integration Views (`backend/igny8_core/modules/integration/views.py`)
|
||||
|
||||
**Change**: Improved error messages to provide more helpful feedback:
|
||||
|
||||
- If API key not configured on site: "API key not configured for this site. Please generate an API key in the IGNY8 app and ensure it is saved to the site."
|
||||
- If API key doesn't match: "Invalid API key. The provided API key does not match the one stored for this site."
|
||||
|
||||
**Result**: Users get clearer error messages when authentication fails.
|
||||
|
||||
## Backend Authentication Details
|
||||
|
||||
### Test-Connection Endpoint
|
||||
- **URL**: `POST /api/v1/integration/integrations/test-connection/`
|
||||
- **Permission**: `AllowAny` (no authentication required via DRF auth classes)
|
||||
- **Authentication Logic**:
|
||||
1. Check if user is authenticated via session/JWT and site belongs to user's account
|
||||
2. If not, check if API key in request body matches site's `wp_api_key` field
|
||||
3. If neither, return 403 error
|
||||
|
||||
### API Key Authentication Class
|
||||
- **Class**: `APIKeyAuthentication` in `backend/igny8_core/api/authentication.py`
|
||||
- **Method**: Validates API key from `Authorization: Bearer {api_key}` header
|
||||
- **Usage**: Used for authenticated API requests after initial connection
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [x] Plugin can connect with valid Site ID and API Key
|
||||
- [x] Plugin shows appropriate error for invalid Site ID
|
||||
- [x] Plugin shows appropriate error for invalid API Key
|
||||
- [x] Plugin shows appropriate error when API key not configured on site
|
||||
- [x] API key is stored securely after successful connection
|
||||
- [x] Subsequent API requests use stored API key for authentication
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **API Key Storage**: API keys are stored using secure storage helpers when available (`igny8_store_secure_option`)
|
||||
2. **API Key Transmission**: API keys are sent in both request body and Authorization header for test-connection
|
||||
3. **Validation**: Backend validates API key matches site's stored key before allowing connection
|
||||
4. **Error Messages**: Error messages don't leak sensitive information about API key format or site existence
|
||||
|
||||
## Related Files
|
||||
|
||||
- `igy8-wp-plugin/includes/class-igny8-api.php` - API client class
|
||||
- `igy8-wp-plugin/admin/class-admin.php` - Admin interface and connection handling
|
||||
- `backend/igny8_core/modules/integration/views.py` - Backend test-connection endpoint
|
||||
- `backend/igny8_core/api/authentication.py` - Backend authentication classes
|
||||
|
||||
@@ -1,909 +0,0 @@
|
||||
# Complete IGNY8 → WordPress Content Publication Audit
|
||||
|
||||
**Date:** November 29, 2025
|
||||
**Scope:** End-to-end analysis of content publishing from IGNY8 backend to WordPress plugin
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
1. [Publication Flow Architecture](#publication-flow-architecture)
|
||||
2. [Publication Triggers](#publication-triggers)
|
||||
3. [Data Fields & Mappings](#data-fields--mappings)
|
||||
4. [WordPress Storage Locations](#wordpress-storage-locations)
|
||||
5. [Sync Functions & Triggers](#sync-functions--triggers)
|
||||
6. [Status Mapping](#status-mapping)
|
||||
7. [Technical Deep Dive](#technical-deep-dive)
|
||||
|
||||
---
|
||||
|
||||
## Publication Flow Architecture
|
||||
|
||||
### High-Level Flow Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ IGNY8 BACKEND (Django) │
|
||||
│ │
|
||||
│ 1. Content Generated in Writer Module │
|
||||
│ └─> ContentPost Model (id, title, content_html, sectors, clusters) │
|
||||
│ │
|
||||
│ 2. Status Changed to "completed" / "published" │
|
||||
│ └─> Triggers: process_pending_wordpress_publications() [Celery] │
|
||||
│ │
|
||||
│ 3. Celery Task: publish_content_to_wordpress │
|
||||
│ └─> Prepares content data payload │
|
||||
│ ├─ Basic Fields: id, title, content_html, excerpt │
|
||||
│ ├─ Metadata: seo_title, seo_description, published_at │
|
||||
│ ├─ Media: featured_image_url, gallery_images │
|
||||
│ ├─ Relations: sectors[], clusters[], tags[], focus_keywords[] │
|
||||
│ └─ Writer Info: author_email, author_name │
|
||||
│ │
|
||||
│ 4. REST API Call (POST) │
|
||||
│ └─> http://wordpress.site/wp-json/igny8/v1/publish-content/ │
|
||||
│ Headers: X-IGNY8-API-KEY, Content-Type: application/json │
|
||||
│ Body: { content_id, task_id, title, content_html, ... } │
|
||||
│ │
|
||||
└──────────────────────────────────────┬──────────────────────────────────┘
|
||||
│
|
||||
│ HTTP POST (30s timeout)
|
||||
│
|
||||
┌──────────────────────────────────────▼──────────────────────────────────┐
|
||||
│ WORDPRESS PLUGIN (igny8-bridge) │
|
||||
│ │
|
||||
│ REST Endpoint: /wp-json/igny8/v1/publish-content/ │
|
||||
│ Handler: Igny8RestAPI::publish_content_to_wordpress() │
|
||||
│ │
|
||||
│ 5. Receive & Validate Data │
|
||||
│ ├─ Check API key in X-IGNY8-API-KEY header │
|
||||
│ ├─ Validate required fields (title, content_html, content_id) │
|
||||
│ ├─ Check connection enabled & Writer module enabled │
|
||||
│ └─ Return 400/401/403 if validation fails │
|
||||
│ │
|
||||
│ 6. Fetch Full Content (if needed) │
|
||||
│ └─> If only content_id provided, call /writer/tasks/{task_id}/ │
|
||||
│ │
|
||||
│ 7. Transform to WordPress Format │
|
||||
│ └─> Call igny8_create_wordpress_post_from_task($content_data) │
|
||||
│ ├─ Prepare post data array (wp_insert_post format) │
|
||||
│ ├─ Resolve post type (post, page, product, custom) │
|
||||
│ ├─ Map IGNY8 status → WordPress status │
|
||||
│ ├─ Set author (by email or default admin) │
|
||||
│ └─ Handle images, meta, taxonomies │
|
||||
│ │
|
||||
│ 8. Create WordPress Post │
|
||||
│ └─> wp_insert_post() → returns post_id │
|
||||
│ Storage: │
|
||||
│ ├─ wp_posts table (main post data) │
|
||||
│ ├─ wp_postmeta table (IGNY8 tracking meta) │
|
||||
│ ├─ wp_posts_term_relationships (taxonomies) │
|
||||
│ └─ wp_posts_attachment_relations (images) │
|
||||
│ │
|
||||
│ 9. Process Related Data │
|
||||
│ ├─ SEO Metadata (Yoast, AIOSEO, SEOPress support) │
|
||||
│ ├─ Featured Image (download & attach) │
|
||||
│ ├─ Gallery Images (add to post gallery) │
|
||||
│ ├─ Categories (create/assign via taxonomy) │
|
||||
│ ├─ Tags (create/assign via taxonomy) │
|
||||
│ ├─ Sectors (map to igny8_sectors custom taxonomy) │
|
||||
│ └─ Clusters (map to igny8_clusters custom taxonomy) │
|
||||
│ │
|
||||
│ 10. Store IGNY8 References (Post Meta) │
|
||||
│ ├─ _igny8_task_id: IGNY8 writer task ID │
|
||||
│ ├─ _igny8_content_id: IGNY8 content ID │
|
||||
│ ├─ _igny8_cluster_id: Associated cluster ID │
|
||||
│ ├─ _igny8_sector_id: Associated sector ID │
|
||||
│ ├─ _igny8_content_type: IGNY8 content type (post, page, etc) │
|
||||
│ ├─ _igny8_content_structure: (article, guide, etc) │
|
||||
│ ├─ _igny8_source: Content source information │
|
||||
│ ├─ _igny8_keyword_ids: Array of associated keyword IDs │
|
||||
│ ├─ _igny8_wordpress_status: Current WordPress status │
|
||||
│ └─ _igny8_last_synced: Timestamp of last update │
|
||||
│ │
|
||||
│ 11. Report Back to IGNY8 │
|
||||
│ └─> HTTP PUT /writer/tasks/{task_id}/ │
|
||||
│ Body: { │
|
||||
│ assigned_post_id: {post_id}, │
|
||||
│ post_url: "https://site.com/post", │
|
||||
│ wordpress_status: "publish", │
|
||||
│ status: "completed", │
|
||||
│ synced_at: "2025-11-29T10:15:30Z", │
|
||||
│ post_type: "post", │
|
||||
│ content_type: "blog" │
|
||||
│ } │
|
||||
│ │
|
||||
│ 12. Return Success Response │
|
||||
│ └─> HTTP 201 Created │
|
||||
│ { │
|
||||
│ success: true, │
|
||||
│ data: { │
|
||||
│ post_id: {post_id}, │
|
||||
│ post_url: "https://site.com/post", │
|
||||
│ post_status: "publish", │
|
||||
│ content_id: {content_id}, │
|
||||
│ task_id: {task_id} │
|
||||
│ }, │
|
||||
│ message: "Content successfully published to WordPress", │
|
||||
│ request_id: "uuid" │
|
||||
│ } │
|
||||
│ │
|
||||
│ 13. Update IGNY8 Model (Backend) │
|
||||
│ ├─ wordpress_sync_status = "success" │
|
||||
│ ├─ wordpress_post_id = {post_id} │
|
||||
│ ├─ wordpress_post_url = "https://site.com/post" │
|
||||
│ ├─ last_wordpress_sync = now() │
|
||||
│ └─ Save to ContentPost model │
|
||||
│ │
|
||||
│ ✓ PUBLICATION COMPLETE │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Publication Triggers
|
||||
|
||||
### Trigger 1: Celery Scheduled Task (Every 5 minutes)
|
||||
|
||||
**Function:** `process_pending_wordpress_publications()` in `igny8_core/tasks/wordpress_publishing.py`
|
||||
|
||||
**Trigger Mechanism:**
|
||||
```python
|
||||
# Runs periodically (configured in celerybeat)
|
||||
@shared_task
|
||||
def process_pending_wordpress_publications() -> Dict[str, Any]:
|
||||
"""
|
||||
Process all content items pending WordPress publication
|
||||
Runs every 5 minutes
|
||||
"""
|
||||
pending_content = ContentPost.objects.filter(
|
||||
wordpress_sync_status='pending',
|
||||
published_at__isnull=False # Only published content
|
||||
)
|
||||
|
||||
# For each pending content → queue publish_content_to_wordpress.delay()
|
||||
```
|
||||
|
||||
**When Triggered:**
|
||||
- Content status becomes `completed` and `published_at` is set
|
||||
- Content not yet sent to WordPress (`wordpress_sync_status == 'pending'`)
|
||||
- Runs automatically every 5 minutes via Celery Beat
|
||||
|
||||
---
|
||||
|
||||
### Trigger 2: Direct REST API Call (Manual/IGNY8 Frontend)
|
||||
|
||||
**Endpoint:** `POST /wp-json/igny8/v1/publish-content/`
|
||||
|
||||
**Handler:** `Igny8RestAPI::publish_content_to_wordpress()`
|
||||
|
||||
**When Called:**
|
||||
- Manual publication from IGNY8 frontend UI
|
||||
- Admin triggers "Publish to WordPress" action
|
||||
- Via IGNY8 backend integration workflow
|
||||
|
||||
---
|
||||
|
||||
### Trigger 3: Webhook from IGNY8 (Event-Based)
|
||||
|
||||
**Handler:** `Igny8Webhooks::handle_task_published()` in `includes/class-igny8-webhooks.php`
|
||||
|
||||
**When Triggered:**
|
||||
- IGNY8 sends webhook when task status → `completed`
|
||||
- Event type: `task.published` or `content.published`
|
||||
- Real-time notification from IGNY8 backend
|
||||
|
||||
---
|
||||
|
||||
## Data Fields & Mappings
|
||||
|
||||
### Complete Field Mapping Table
|
||||
|
||||
| IGNY8 Field | IGNY8 Type | WordPress Storage | WordPress Field/Meta | Notes |
|
||||
|---|---|---|---|---|
|
||||
| **Core Content** | | | | |
|
||||
| `id` | int | postmeta | `_igny8_task_id` OR `_igny8_content_id` | Primary identifier |
|
||||
| `title` | string | posts | `post_title` | Post title |
|
||||
| `content_html` | string | posts | `post_content` | Main content (HTML) |
|
||||
| `content` | string | posts | `post_content` | Fallback if `content_html` missing |
|
||||
| `brief` / `excerpt` | string | posts | `post_excerpt` | Post excerpt |
|
||||
| **Status & Publishing** | | | | |
|
||||
| `status` | enum | posts | `post_status` | See Status Mapping table |
|
||||
| `published_at` | datetime | posts | `post_date` | Publication date |
|
||||
| `status` | string | postmeta | `_igny8_wordpress_status` | WP status snapshot |
|
||||
| **Content Classification** | | | | |
|
||||
| `content_type` | string | postmeta | `_igny8_content_type` | Type: post, page, article, blog |
|
||||
| `content_structure` | string | postmeta | `_igny8_content_structure` | Structure: article, guide, etc |
|
||||
| `post_type` | string | posts | `post_type` | WordPress post type |
|
||||
| **Relationships** | | | | |
|
||||
| `cluster_id` | int | postmeta | `_igny8_cluster_id` | Primary cluster |
|
||||
| `sector_id` | int | postmeta | `_igny8_sector_id` | Primary sector |
|
||||
| `clusters[]` | array | tax | `igny8_clusters` | Custom taxonomy terms |
|
||||
| `sectors[]` | array | tax | `igny8_sectors` | Custom taxonomy terms |
|
||||
| `keyword_ids[]` | array | postmeta | `_igny8_keyword_ids` | Array of keyword IDs |
|
||||
| **Categories & Tags** | | | | |
|
||||
| `categories[]` | array | tax | `category` | Standard WP categories |
|
||||
| `tags[]` | array | tax | `post_tag` | Standard WP tags |
|
||||
| **Author** | | | | |
|
||||
| `author_email` | string | posts | `post_author` | Map to WP user by email |
|
||||
| `author_name` | string | posts | `post_author` | Fallback if email not found |
|
||||
| **Media** | | | | |
|
||||
| `featured_image_url` | string | postmeta | `_thumbnail_id` | Downloaded & attached |
|
||||
| `featured_image` | object | postmeta | `_thumbnail_id` | Object with URL, alt text |
|
||||
| `gallery_images[]` | array | postmeta | `_igny8_gallery_images` | Array of image URLs/data |
|
||||
| **SEO Metadata** | | | | |
|
||||
| `seo_title` | string | postmeta | Yoast: `_yoast_wpseo_title` | SEO plugin support |
|
||||
| | | postmeta | AIOSEO: `_aioseo_title` | All-in-One SEO |
|
||||
| | | postmeta | SEOPress: `_seopress_titles_title` | SEOPress |
|
||||
| | | postmeta | Generic: `_igny8_meta_title` | Fallback |
|
||||
| `seo_description` | string | postmeta | Yoast: `_yoast_wpseo_metadesc` | Meta description |
|
||||
| | | postmeta | AIOSEO: `_aioseo_description` | All-in-One SEO |
|
||||
| | | postmeta | SEOPress: `_seopress_titles_desc` | SEOPress |
|
||||
| | | postmeta | Generic: `_igny8_meta_description` | Fallback |
|
||||
| **Additional Fields** | | | | |
|
||||
| `source` | string | postmeta | `_igny8_source` | Content source |
|
||||
| `focus_keywords[]` | array | postmeta | `_igny8_focus_keywords` | SEO keywords |
|
||||
| **Sync Metadata** | | | | |
|
||||
| `task_id` | int | postmeta | `_igny8_task_id` | IGNY8 task ID |
|
||||
| `content_id` | int | postmeta | `_igny8_content_id` | IGNY8 content ID |
|
||||
| (generated) | — | postmeta | `_igny8_last_synced` | Last sync timestamp |
|
||||
| (generated) | — | postmeta | `_igny8_brief_cached_at` | Brief cache timestamp |
|
||||
|
||||
---
|
||||
|
||||
### Data Payload Sent from IGNY8 to WordPress
|
||||
|
||||
**HTTP Request Format:**
|
||||
|
||||
```http
|
||||
POST /wp-json/igny8/v1/publish-content/ HTTP/1.1
|
||||
Host: wordpress.site
|
||||
Content-Type: application/json
|
||||
X-IGNY8-API-KEY: {{api_key_from_wordpress_plugin}}
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## WordPress Storage Locations
|
||||
|
||||
### 1. WordPress Posts Table (`wp_posts`)
|
||||
|
||||
**Core post data stored directly in posts table:**
|
||||
|
||||
| Column | IGNY8 Source | Example Value |
|
||||
|--------|---|---|
|
||||
| `ID` | (generated by WP) | 1842 |
|
||||
| `post_title` | `title` | "Advanced SEO Strategies for 2025" |
|
||||
| `post_content` | `content_html` / `content` | `<p>HTML content...</p>` |
|
||||
| `post_excerpt` | `excerpt` / `brief` | "Learn SEO strategies..." |
|
||||
| `post_status` | `status` (mapped) | `publish` |
|
||||
| `post_type` | Resolved from `content_type` | `post` |
|
||||
| `post_author` | `author_email` (lookup user ID) | `3` (admin user ID) |
|
||||
| `post_date` | `published_at` | `2025-11-29 10:15:30` |
|
||||
| `post_date_gmt` | `published_at` (GMT) | `2025-11-29 10:15:30` |
|
||||
|
||||
**Retrieval Query:**
|
||||
```php
|
||||
$post = get_post($post_id);
|
||||
echo $post->post_title; // "Advanced SEO Strategies for 2025"
|
||||
echo $post->post_content; // HTML content
|
||||
echo $post->post_status; // "publish"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. WordPress Post Meta Table (`wp_postmeta`)
|
||||
|
||||
**IGNY8 tracking and metadata stored as post meta:**
|
||||
|
||||
| Meta Key | Meta Value | Example | Purpose |
|
||||
|----------|-----------|---------|---------|
|
||||
| `_igny8_task_id` | int | `15` | Link to IGNY8 writer task |
|
||||
| `_igny8_content_id` | int | `42` | Link to IGNY8 content |
|
||||
| `_igny8_cluster_id` | int | `12` | Primary cluster reference |
|
||||
| `_igny8_sector_id` | int | `5` | Primary sector reference |
|
||||
| `_igny8_content_type` | string | `"blog"` | IGNY8 content type |
|
||||
| `_igny8_content_structure` | string | `"article"` | Content structure type |
|
||||
| `_igny8_source` | string | `"writer_module"` | Content origin |
|
||||
| `_igny8_keyword_ids` | serialized array | `a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}` | Associated keywords |
|
||||
| `_igny8_wordpress_status` | string | `"publish"` | Last known WP status |
|
||||
| `_igny8_last_synced` | datetime | `2025-11-29 10:15:30` | Last sync timestamp |
|
||||
| `_igny8_task_brief` | JSON string | `{...}` | Cached task brief |
|
||||
| `_igny8_brief_cached_at` | datetime | `2025-11-29 10:20:00` | Brief cache time |
|
||||
| **SEO Meta** | | | |
|
||||
| `_yoast_wpseo_title` | string | `"Advanced SEO Strategies for 2025 \| Your Site"` | Yoast SEO title |
|
||||
| `_yoast_wpseo_metadesc` | string | `"Learn the best SEO practices for ranking in 2025"` | Yoast meta desc |
|
||||
| `_aioseo_title` | string | `"Advanced SEO Strategies for 2025 \| Your Site"` | AIOSEO title |
|
||||
| `_aioseo_description` | string | `"Learn the best SEO practices for ranking in 2025"` | AIOSEO description |
|
||||
| `_seopress_titles_title` | string | `"Advanced SEO Strategies for 2025 \| Your Site"` | SEOPress title |
|
||||
| `_seopress_titles_desc` | string | `"Learn the best SEO practices for ranking in 2025"` | SEOPress desc |
|
||||
| **Generic Fallbacks** | | | |
|
||||
| `_igny8_meta_title` | string | `"Advanced SEO Strategies for 2025"` | Generic SEO title |
|
||||
| `_igny8_meta_description` | string | `"Learn the best SEO practices for ranking in 2025"` | Generic SEO desc |
|
||||
| `_igny8_focus_keywords` | serialized array | `a:3:{...}` | SEO focus keywords |
|
||||
| **Media** | | | |
|
||||
| `_thumbnail_id` | int | `1842` | Featured image attachment ID |
|
||||
| `_igny8_gallery_images` | serialized array | `a:5:{...}` | Gallery image attachment IDs |
|
||||
|
||||
**Retrieval Query:**
|
||||
```php
|
||||
// Get IGNY8 metadata
|
||||
$task_id = get_post_meta($post_id, '_igny8_task_id', true); // 15
|
||||
$content_id = get_post_meta($post_id, '_igny8_content_id', true); // 42
|
||||
$cluster_id = get_post_meta($post_id, '_igny8_cluster_id', true); // 12
|
||||
$keyword_ids = get_post_meta($post_id, '_igny8_keyword_ids', true); // array
|
||||
|
||||
// Get SEO metadata
|
||||
$seo_title = get_post_meta($post_id, '_yoast_wpseo_title', true);
|
||||
$seo_desc = get_post_meta($post_id, '_yoast_wpseo_metadesc', true);
|
||||
|
||||
// Get last sync info
|
||||
$last_synced = get_post_meta($post_id, '_igny8_last_synced', true);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. WordPress Taxonomies (`wp_terms` & `wp_term_relationships`)
|
||||
|
||||
**Categories and Tags:**
|
||||
|
||||
```sql
|
||||
-- Categories
|
||||
SELECT * FROM wp_terms t
|
||||
JOIN wp_term_taxonomy tt ON t.term_id = tt.term_id
|
||||
JOIN wp_term_relationships tr ON tt.term_taxonomy_id = tr.term_taxonomy_id
|
||||
WHERE tt.taxonomy = 'category' AND tr.object_id = {post_id};
|
||||
|
||||
-- Tags
|
||||
SELECT * FROM wp_terms t
|
||||
JOIN wp_term_taxonomy tt ON t.term_id = tt.term_id
|
||||
JOIN wp_term_relationships tr ON tt.term_taxonomy_id = tr.term_taxonomy_id
|
||||
WHERE tt.taxonomy = 'post_tag' AND tr.object_id = {post_id};
|
||||
```
|
||||
|
||||
**Retrieval Query:**
|
||||
```php
|
||||
// Get categories
|
||||
$categories = wp_get_post_terms($post_id, 'category', array('fields' => 'all'));
|
||||
foreach ($categories as $cat) {
|
||||
echo $cat->name; // "Digital Marketing"
|
||||
echo $cat->slug; // "digital-marketing"
|
||||
}
|
||||
|
||||
// Get tags
|
||||
$tags = wp_get_post_terms($post_id, 'post_tag', array('fields' => 'all'));
|
||||
foreach ($tags as $tag) {
|
||||
echo $tag->name; // "seo"
|
||||
echo $tag->slug; // "seo"
|
||||
}
|
||||
```
|
||||
|
||||
**Custom Taxonomies (IGNY8-specific):**
|
||||
|
||||
```php
|
||||
// Sectors taxonomy
|
||||
wp_set_post_terms($post_id, [5, 8], 'igny8_sectors');
|
||||
|
||||
// Clusters taxonomy
|
||||
wp_set_post_terms($post_id, [12, 15], 'igny8_clusters');
|
||||
|
||||
// Retrieval
|
||||
$sectors = wp_get_post_terms($post_id, 'igny8_sectors', array('fields' => 'all'));
|
||||
$clusters = wp_get_post_terms($post_id, 'igny8_clusters', array('fields' => 'all'));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Featured Image (Post Attachment)
|
||||
|
||||
**Process:**
|
||||
|
||||
1. Download image from `featured_image_url`
|
||||
2. Upload to WordPress media library
|
||||
3. Create attachment post
|
||||
4. Set `_thumbnail_id` post meta to attachment ID
|
||||
|
||||
**Storage:**
|
||||
|
||||
```php
|
||||
// Query featured image
|
||||
$thumbnail_id = get_post_thumbnail_id($post_id);
|
||||
$image_url = wp_get_attachment_image_url($thumbnail_id, 'full');
|
||||
$image_alt = get_post_meta($thumbnail_id, '_wp_attachment_image_alt', true);
|
||||
|
||||
// In HTML
|
||||
echo get_the_post_thumbnail($post_id, 'medium');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Gallery Images
|
||||
|
||||
**Storage Method:**
|
||||
|
||||
- Downloaded images stored as attachments
|
||||
- Image IDs stored in `_igny8_gallery_images` post meta
|
||||
- Can be serialized array or JSON
|
||||
|
||||
```php
|
||||
// Store gallery images
|
||||
$gallery_ids = [1842, 1843, 1844, 1845, 1846]; // 5 images max
|
||||
update_post_meta($post_id, '_igny8_gallery_images', $gallery_ids);
|
||||
|
||||
// Retrieve gallery images
|
||||
$gallery_ids = get_post_meta($post_id, '_igny8_gallery_images', true);
|
||||
foreach ($gallery_ids as $img_id) {
|
||||
echo wp_get_attachment_image($img_id, 'medium');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sync Functions & Triggers
|
||||
|
||||
### Core Sync Functions
|
||||
|
||||
#### 1. `publish_content_to_wordpress()` [IGNY8 Backend - Celery Task]
|
||||
|
||||
**File:** `igny8_core/tasks/wordpress_publishing.py`
|
||||
|
||||
**Trigger:** Every 5 minutes via Celery Beat
|
||||
|
||||
**Flow:**
|
||||
```python
|
||||
@shared_task(bind=True, max_retries=3)
|
||||
def publish_content_to_wordpress(self, content_id: int, site_integration_id: int,
|
||||
task_id: Optional[int] = None) -> Dict[str, Any]:
|
||||
# 1. Get ContentPost and SiteIntegration models
|
||||
# 2. Check if already published (wordpress_sync_status == 'success')
|
||||
# 3. Set status to 'syncing'
|
||||
# 4. Prepare content_data payload
|
||||
# 5. POST to WordPress REST API
|
||||
# 6. Handle response:
|
||||
# - 201: Success → store post_id, post_url, update status to 'success'
|
||||
# - 409: Already exists → update status to 'success'
|
||||
# - Other: Retry with exponential backoff (1min, 5min, 15min)
|
||||
# 7. Update ContentPost model
|
||||
return {"success": True, "wordpress_post_id": post_id, "wordpress_post_url": url}
|
||||
```
|
||||
|
||||
**Retry Logic:**
|
||||
- Max retries: 3
|
||||
- Backoff: 1 minute, 5 minutes, 15 minutes
|
||||
- After max retries: Set status to `failed`
|
||||
|
||||
---
|
||||
|
||||
#### 2. `igny8_create_wordpress_post_from_task()` [WordPress Plugin]
|
||||
|
||||
**File:** `sync/igny8-to-wp.php`
|
||||
|
||||
**Trigger:**
|
||||
- Called from REST API endpoint
|
||||
- Called from webhook handler
|
||||
- Called from manual sync
|
||||
|
||||
**Flow:**
|
||||
```php
|
||||
function igny8_create_wordpress_post_from_task($content_data, $allowed_post_types = array()) {
|
||||
// 1. Resolve post type (post, page, product, custom)
|
||||
// 2. Check if post type is enabled
|
||||
// 3. Prepare post_data array:
|
||||
// - post_title (sanitized)
|
||||
// - post_content (kses_post for HTML)
|
||||
// - post_excerpt
|
||||
// - post_status (from IGNY8 status mapping)
|
||||
// - post_type
|
||||
// - post_author (resolved from email or default)
|
||||
// - post_date (from published_at)
|
||||
// - meta_input (all _igny8_* meta)
|
||||
// 4. wp_insert_post() → get post_id
|
||||
// 5. Process media:
|
||||
// - igny8_import_seo_metadata()
|
||||
// - igny8_import_featured_image()
|
||||
// - igny8_import_taxonomies()
|
||||
// - igny8_import_content_images()
|
||||
// 6. Assign custom taxonomies (sectors, clusters)
|
||||
// 7. Assign categories and tags
|
||||
// 8. Store IGNY8 references in post meta
|
||||
// 9. Update IGNY8 task via API (PUT /writer/tasks/{id}/)
|
||||
// 10. Return post_id
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 3. `igny8_sync_igny8_tasks_to_wp()` [WordPress Plugin - Batch Sync]
|
||||
|
||||
**File:** `sync/igny8-to-wp.php`
|
||||
|
||||
**Trigger:**
|
||||
- Manual sync button in admin
|
||||
- Scheduled cron job (optional)
|
||||
- Initial site setup
|
||||
|
||||
**Flow:**
|
||||
```php
|
||||
function igny8_sync_igny8_tasks_to_wp($filters = array()) {
|
||||
// 1. Check connection enabled & authenticated
|
||||
// 2. Get enabled post types
|
||||
// 3. Build API endpoint: /writer/tasks/?site_id={id}&status={status}&cluster_id={id}
|
||||
// 4. GET from IGNY8 API → get tasks array
|
||||
// 5. For each task:
|
||||
// a. Check if post exists (by _igny8_task_id meta)
|
||||
// b. If exists:
|
||||
// - wp_update_post() with new title, content, status
|
||||
// - Update categories, tags, images
|
||||
// - Increment $updated counter
|
||||
// c. If not exists:
|
||||
// - Check if post_type is allowed
|
||||
// - igny8_create_wordpress_post_from_task()
|
||||
// - Increment $created counter
|
||||
// 6. Return { success, created, updated, failed, skipped, total }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### WordPress Hooks (Two-Way Sync)
|
||||
|
||||
#### Hook 1: `save_post` [WordPress → IGNY8]
|
||||
|
||||
**File:** `docs/WORDPRESS-PLUGIN-INTEGRATION.md` & implementation in plugin
|
||||
|
||||
**When Triggered:** Post is saved (any status change)
|
||||
|
||||
**Actions:**
|
||||
```php
|
||||
add_action('save_post', function($post_id) {
|
||||
// 1. Check if IGNY8-managed (has _igny8_task_id)
|
||||
// 2. Get task_id from post meta
|
||||
// 3. Map WordPress status → IGNY8 status
|
||||
// 4. PUT /writer/tasks/{task_id}/ with:
|
||||
// - status: mapped IGNY8 status
|
||||
// - assigned_post_id: WordPress post ID
|
||||
// - post_url: permalink
|
||||
}, 10, 1);
|
||||
```
|
||||
|
||||
**Status Map:**
|
||||
- `publish` → `completed`
|
||||
- `draft` → `draft`
|
||||
- `pending` → `pending`
|
||||
- `private` → `completed`
|
||||
- `trash` → `archived`
|
||||
- `future` → `scheduled`
|
||||
|
||||
---
|
||||
|
||||
#### Hook 2: `publish_post` [WordPress → IGNY8 + Keywords]
|
||||
|
||||
**File:** `docs/WORDPRESS-PLUGIN-INTEGRATION.md`
|
||||
|
||||
**When Triggered:** Post changes to `publish` status
|
||||
|
||||
**Actions:**
|
||||
```php
|
||||
add_action('publish_post', function($post_id) {
|
||||
// 1. Get _igny8_task_id from post meta
|
||||
// 2. GET /writer/tasks/{task_id}/ to get cluster_id
|
||||
// 3. GET /planner/keywords/?cluster_id={cluster_id}
|
||||
// 4. For each keyword: PUT /planner/keywords/{id}/ { status: 'mapped' }
|
||||
// 5. Update task status to 'completed'
|
||||
}, 10, 1);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Hook 3: `transition_post_status` [WordPress → IGNY8]
|
||||
|
||||
**File:** `sync/hooks.php` & `docs/WORDPRESS-PLUGIN-INTEGRATION.md`
|
||||
|
||||
**When Triggered:** Post status changes
|
||||
|
||||
**Actions:**
|
||||
```php
|
||||
add_action('transition_post_status', function($new_status, $old_status, $post) {
|
||||
if ($new_status === $old_status) return;
|
||||
|
||||
$task_id = get_post_meta($post->ID, '_igny8_task_id', true);
|
||||
if (!$task_id) return;
|
||||
|
||||
// Map status and PUT to IGNY8
|
||||
$igny8_status = igny8_map_wp_status_to_igny8($new_status);
|
||||
|
||||
$api->put("/writer/tasks/{$task_id}/", [
|
||||
'status' => $igny8_status,
|
||||
'assigned_post_id' => $post->ID,
|
||||
'post_url' => get_permalink($post->ID)
|
||||
]);
|
||||
}, 10, 3);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Hook 4: Webhook Handler [IGNY8 → WordPress]
|
||||
|
||||
**File:** `includes/class-igny8-webhooks.php`
|
||||
|
||||
**Endpoint:** `POST /wp-json/igny8/v1/webhook/`
|
||||
|
||||
**Webhook Event Types:**
|
||||
- `task.published` / `task.completed`
|
||||
- `content.published`
|
||||
|
||||
**Handler:**
|
||||
```php
|
||||
public function handle_task_published($data) {
|
||||
$task_id = $data['task_id'];
|
||||
|
||||
// Check if post exists (by _igny8_task_id)
|
||||
$existing_posts = get_posts([
|
||||
'meta_key' => '_igny8_task_id',
|
||||
'meta_value' => $task_id,
|
||||
'post_type' => 'any',
|
||||
'posts_per_page' => 1
|
||||
]);
|
||||
|
||||
if (!empty($existing_posts)) {
|
||||
// Update status if needed
|
||||
wp_update_post([
|
||||
'ID' => $existing_posts[0]->ID,
|
||||
'post_status' => $data['status'] === 'publish' ? 'publish' : 'draft'
|
||||
]);
|
||||
} else {
|
||||
// Create new post
|
||||
$api->get("/writer/tasks/{$task_id}/");
|
||||
igny8_create_wordpress_post_from_task($content_data, $enabled_post_types);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Status Mapping
|
||||
|
||||
### IGNY8 Status ↔ WordPress Status
|
||||
|
||||
| IGNY8 Status | WordPress Status | Description | Sync Direction |
|
||||
|---|---|---|---|
|
||||
| `draft` | `draft` | Content is draft | ↔ Bidirectional |
|
||||
| `completed` | `publish` | Content published/completed | ↔ Bidirectional |
|
||||
| `pending` | `pending` | Content pending review | ↔ Bidirectional |
|
||||
| `scheduled` | `future` | Content scheduled for future | → IGNY8 only |
|
||||
| `archived` | `trash` | Content archived/deleted | → IGNY8 only |
|
||||
| (WP publish) | `publish` | WordPress post published | → IGNY8 (mapped to `completed`) |
|
||||
|
||||
**Mapping Functions:**
|
||||
|
||||
```php
|
||||
// IGNY8 → WordPress
|
||||
function igny8_map_igny8_status_to_wp($igny8_status) {
|
||||
$map = [
|
||||
'completed' => 'publish',
|
||||
'draft' => 'draft',
|
||||
'pending' => 'pending',
|
||||
'scheduled' => 'future',
|
||||
'archived' => 'trash'
|
||||
];
|
||||
return $map[$igny8_status] ?? 'draft';
|
||||
}
|
||||
|
||||
// WordPress → IGNY8
|
||||
function igny8_map_wp_status_to_igny8($wp_status) {
|
||||
$map = [
|
||||
'publish' => 'completed',
|
||||
'draft' => 'draft',
|
||||
'pending' => 'pending',
|
||||
'private' => 'completed',
|
||||
'trash' => 'archived',
|
||||
'future' => 'scheduled'
|
||||
];
|
||||
return $map[$wp_status] ?? 'draft';
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Technical Deep Dive
|
||||
|
||||
### API Authentication Flow
|
||||
|
||||
**IGNY8 Backend → WordPress:**
|
||||
|
||||
1. WordPress Admin stores API key: `Settings → IGNY8 → API Key`
|
||||
- Stored in `igny8_api_key` option
|
||||
- May be encrypted if `igny8_get_secure_option()` available
|
||||
|
||||
2. WordPress Plugin stores in REST API response:
|
||||
- `GET /wp-json/igny8/v1/status` returns `has_api_key: true/false`
|
||||
|
||||
3. IGNY8 Backend stores WordPress API key:
|
||||
- In `SiteIntegration.api_key` field
|
||||
- Sent in every request as `X-IGNY8-API-KEY` header
|
||||
|
||||
4. WordPress Plugin validates:
|
||||
```php
|
||||
public function check_permission($request) {
|
||||
$header_api_key = $request->get_header('x-igny8-api-key');
|
||||
$stored_api_key = igny8_get_secure_option('igny8_api_key');
|
||||
|
||||
if ($stored_api_key && hash_equals($stored_api_key, $header_api_key)) {
|
||||
return true; // Authenticated
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Error Handling & Retry Logic
|
||||
|
||||
**IGNY8 Backend Celery Task Retries:**
|
||||
|
||||
```python
|
||||
@shared_task(bind=True, max_retries=3)
|
||||
def publish_content_to_wordpress(self, content_id, ...):
|
||||
try:
|
||||
response = requests.post(wordpress_url, json=content_data, timeout=30)
|
||||
|
||||
if response.status_code == 201:
|
||||
# Success
|
||||
content.wordpress_sync_status = 'success'
|
||||
content.save()
|
||||
return {"success": True}
|
||||
|
||||
elif response.status_code == 409:
|
||||
# Conflict - content already exists
|
||||
content.wordpress_sync_status = 'success'
|
||||
return {"success": True, "message": "Already exists"}
|
||||
|
||||
else:
|
||||
# Retry with exponential backoff
|
||||
if self.request.retries < self.max_retries:
|
||||
countdown = 60 * (5 ** self.request.retries) # 1min, 5min, 15min
|
||||
raise self.retry(countdown=countdown, exc=Exception(error_msg))
|
||||
else:
|
||||
# Max retries reached
|
||||
content.wordpress_sync_status = 'failed'
|
||||
content.save()
|
||||
return {"success": False, "error": error_msg}
|
||||
|
||||
except Exception as e:
|
||||
content.wordpress_sync_status = 'failed'
|
||||
content.save()
|
||||
return {"success": False, "error": str(e)}
|
||||
```
|
||||
|
||||
**WordPress Plugin Response Codes:**
|
||||
|
||||
```
|
||||
201 Created → Success, post created
|
||||
409 Conflict → Content already exists (OK)
|
||||
400 Bad Request → Missing required fields
|
||||
401 Unauthorized → Invalid API key
|
||||
403 Forbidden → Connection disabled
|
||||
404 Not Found → Endpoint not found
|
||||
500 Server Error → Internal WP error
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Cache & Performance
|
||||
|
||||
**Transients (5-minute cache):**
|
||||
|
||||
```php
|
||||
// Site metadata caching
|
||||
$cache_key = 'igny8_site_metadata_v1';
|
||||
$cached = get_transient($cache_key);
|
||||
if ($cached !== false) {
|
||||
return $cached; // Use cache
|
||||
}
|
||||
|
||||
// Cache for 5 minutes
|
||||
set_transient($cache_key, $data, 300);
|
||||
```
|
||||
|
||||
**Query Optimization:**
|
||||
|
||||
```php
|
||||
// Batch checking for existing posts
|
||||
$existing_posts = get_posts([
|
||||
'meta_key' => '_igny8_task_id',
|
||||
'meta_value' => $task_id,
|
||||
'posts_per_page' => 1,
|
||||
'fields' => 'ids' // Only get IDs, not full post objects
|
||||
]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Logging & Debugging
|
||||
|
||||
**Enable Debug Logging:**
|
||||
|
||||
```php
|
||||
// In wp-config.php
|
||||
define('WP_DEBUG', true);
|
||||
define('WP_DEBUG_LOG', true);
|
||||
define('IGNY8_DEBUG', true); // Custom plugin debug flag
|
||||
```
|
||||
|
||||
**Log Locations:**
|
||||
|
||||
- WordPress: `/wp-content/debug.log`
|
||||
- IGNY8 Backend: `logs/` directory (Django settings)
|
||||
|
||||
**Example Logs:**
|
||||
|
||||
```
|
||||
[2025-11-29 10:15:30] IGNY8: Created WordPress post 1842 from task 15
|
||||
[2025-11-29 10:15:31] IGNY8: Updated task 15 with WordPress post ID 1842
|
||||
[2025-11-29 10:15:35] IGNY8: Synced post 1842 status to task 15: completed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary Table: Complete End-to-End Field Flow
|
||||
|
||||
| Step | IGNY8 Field | Transmitted As | WordPress Storage | Retrieval Method |
|
||||
|---|---|---|---|---|
|
||||
| 1 | Content ID | `content_id` in JSON | `_igny8_content_id` meta | `get_post_meta($pid, '_igny8_content_id')` |
|
||||
| 2 | Title | `title` in JSON | `post_title` column | `get_the_title($post_id)` |
|
||||
| 3 | Content HTML | `content_html` in JSON | `post_content` column | `get_the_content()` or `$post->post_content` |
|
||||
| 4 | Status | `status` in JSON (mapped) | `post_status` column | `get_post_status($post_id)` |
|
||||
| 5 | Author Email | `author_email` in JSON | Lookup user ID → `post_author` | `get_the_author_meta('email', $post->post_author)` |
|
||||
| 6 | Task ID | `task_id` in JSON | `_igny8_task_id` meta | `get_post_meta($pid, '_igny8_task_id')` |
|
||||
| 7 | Cluster ID | `cluster_id` in JSON | `_igny8_cluster_id` meta | `get_post_meta($pid, '_igny8_cluster_id')` |
|
||||
| 8 | Categories | `categories[]` in JSON | `category` taxonomy | `wp_get_post_terms($pid, 'category')` |
|
||||
| 9 | SEO Title | `seo_title` in JSON | Multiple meta keys | `get_post_meta($pid, '_yoast_wpseo_title')` |
|
||||
| 10 | Featured Image | `featured_image_url` in JSON | `_thumbnail_id` meta | `get_post_thumbnail_id($post_id)` |
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The IGNY8 → WordPress integration is a **robust, bidirectional sync** system with:
|
||||
|
||||
✅ **Multiple entry points** (Celery tasks, REST APIs, webhooks)
|
||||
✅ **Comprehensive field mapping** (50+ data points synchronized)
|
||||
✅ **Flexible storage** (posts, postmeta, taxonomies, attachments)
|
||||
✅ **Error handling & retries** (exponential backoff up to 3 retries)
|
||||
✅ **Status synchronization** (6-way bidirectional status mapping)
|
||||
✅ **Media handling** (featured images, galleries, SEO metadata)
|
||||
✅ **Two-way sync hooks** (WordPress changes → IGNY8, IGNY8 changes → WordPress)
|
||||
✅ **Authentication** (API key validation on every request)
|
||||
|
||||
The system ensures data consistency across both platforms while maintaining independence and allowing manual overrides where needed.
|
||||
|
||||
---
|
||||
|
||||
**Generated:** 2025-11-29
|
||||
**Audit Scope:** Complete publication workflow analysis
|
||||
**Coverage:** IGNY8 Backend + WordPress Plugin integration
|
||||
| 8 | Categories | `categories[]` in JSON | `category` taxonomy | `wp_get_post_terms($pid, 'category')` |
|
||||
| 9 | SEO Title | `seo_title` in JSON | Multiple meta keys | `get_post_meta($pid, '_yoast_wpseo_title')` |
|
||||
| 10 | Featured Image | `featured_image_url` in JSON | `_thumbnail_id` meta | `get_post_thumbnail_id($post_id)` |
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The IGNY8 → WordPress integration is a **robust, bidirectional sync** system with:
|
||||
|
||||
✅ **Multiple entry points** (Celery tasks, REST APIs, webhooks)
|
||||
✅ **Comprehensive field mapping** (50+ data points synchronized)
|
||||
✅ **Flexible storage** (posts, postmeta, taxonomies, attachments)
|
||||
✅ **Error handling & retries** (exponential backoff up to 3 retries)
|
||||
✅ **Status synchronization** (6-way bidirectional status mapping)
|
||||
✅ **Media handling** (featured images, galleries, SEO metadata)
|
||||
✅ **Two-way sync hooks** (WordPress changes → IGNY8, IGNY8 changes → WordPress)
|
||||
✅ **Authentication** (API key validation on every request)
|
||||
|
||||
The system ensures data consistency across both platforms while maintaining independence and allowing manual overrides where needed.
|
||||
|
||||
---
|
||||
|
||||
**Generated:** 2025-11-29
|
||||
**Audit Scope:** Complete publication workflow analysis
|
||||
**Coverage:** IGNY8 Backend + WordPress Plugin integration
|
||||
@@ -1,357 +0,0 @@
|
||||
# Debugging Guide - December 1, 2025
|
||||
|
||||
## Issues to Fix
|
||||
|
||||
### Issue 1: Status Not Changing from 'review' to 'published'
|
||||
**Symptom:** Content stays in "review" status in IGNY8 app after clicking Publish button
|
||||
|
||||
**What to check:**
|
||||
1. Go to https://app.igny8.com/settings/debug-status
|
||||
2. Click "Publish" on a content item in Review page
|
||||
3. Look for these log messages in IGNY8 backend logs:
|
||||
- `[publish_content_to_wordpress] 📦 Preparing content payload...`
|
||||
- `Content status: 'review'` or `'published'`
|
||||
- `💾 Content model updated: Status: 'X' → 'published'`
|
||||
|
||||
**Expected flow:**
|
||||
1. User clicks Publish → Status immediately changes to 'published' in IGNY8
|
||||
2. Celery task queues WordPress publish
|
||||
3. WordPress responds with post_id and post_url
|
||||
4. IGNY8 updates external_id and external_url
|
||||
|
||||
### Issue 2: No WP Status Column on Published Page
|
||||
**Symptom:** Published page doesn't show WordPress post status
|
||||
|
||||
**What to check:**
|
||||
- Call: `GET https://app.igny8.com/api/v1/writer/content/{id}/wordpress_status/`
|
||||
- Expected response:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"wordpress_status": "publish",
|
||||
"external_id": 123,
|
||||
"external_url": "https://site.com/post",
|
||||
"post_title": "...",
|
||||
"last_checked": "2025-12-01T..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**WordPress endpoint:**
|
||||
- `GET https://yoursite.com/wp-json/igny8/v1/post-status/{post_id}/`
|
||||
|
||||
### Issue 3: Custom Taxonomy/Attribute Columns Still Showing
|
||||
**Symptom:** WordPress admin shows "Taxonomy" and "Attribute" columns
|
||||
|
||||
**What to check:**
|
||||
1. Go to WordPress admin → Posts → All Posts
|
||||
2. Check column headers
|
||||
3. Should ONLY see: Title, Author, Categories, Tags, Date
|
||||
4. Should NOT see: Taxonomy, Attribute
|
||||
|
||||
**If still showing:** Clear WordPress object cache and refresh page
|
||||
|
||||
### Issue 4: Tags, Categories, Images Not Saving
|
||||
**Symptom:** WordPress posts don't have tags, categories, or images after publishing
|
||||
|
||||
**What to check in logs:**
|
||||
|
||||
#### IGNY8 Backend Logs (Celery worker output):
|
||||
```
|
||||
[publish_content_to_wordpress] Found X taxonomy mappings
|
||||
[publish_content_to_wordpress] 📁 Added category: 'Category Name'
|
||||
[publish_content_to_wordpress] Found X images for content
|
||||
[publish_content_to_wordpress] 🖼️ Featured image: https://...
|
||||
[publish_content_to_wordpress] 🏷️ Primary keyword (tag): 'keyword'
|
||||
[publish_content_to_wordpress] 📊 TOTAL: X categories, Y tags
|
||||
[publish_content_to_wordpress] 📦 Payload summary:
|
||||
- Categories: [...]
|
||||
- Tags: [...]
|
||||
- Featured image: Yes
|
||||
- Gallery images: N
|
||||
```
|
||||
|
||||
#### WordPress Logs (debug.log):
|
||||
```
|
||||
========== IGNY8 PUBLISH REQUEST ==========
|
||||
Content ID: 123
|
||||
Categories: ["Category1","Category2"]
|
||||
Tags: ["tag1","tag2","tag3"]
|
||||
Featured Image: https://...
|
||||
Gallery Images: 2 images
|
||||
===========================================
|
||||
|
||||
========== IGNY8 CREATE WP POST ==========
|
||||
IGNY8: Processing 2 categories
|
||||
IGNY8: ✅ Assigned 2 categories to post 456
|
||||
IGNY8: Processing 3 tags
|
||||
IGNY8: ✅ Assigned 3 tags to post 456
|
||||
IGNY8: Setting featured image from featured_image_url field: https://...
|
||||
IGNY8: Setting gallery with 2 images
|
||||
IGNY8: Setting SEO meta title: ...
|
||||
========== IGNY8 POST CREATION COMPLETE: Post ID 456 ==========
|
||||
```
|
||||
|
||||
## How to Enable Logging
|
||||
|
||||
### IGNY8 Backend
|
||||
1. Celery logs are automatically output to console
|
||||
2. Run Celery worker with: `celery -A igny8_core worker -l info`
|
||||
3. Or check Docker logs: `docker logs -f igny8_celery`
|
||||
|
||||
### WordPress
|
||||
1. Enable debug mode in `wp-config.php`:
|
||||
```php
|
||||
define('WP_DEBUG', true);
|
||||
define('WP_DEBUG_LOG', true);
|
||||
define('WP_DEBUG_DISPLAY', false);
|
||||
```
|
||||
|
||||
2. Check logs at: `wp-content/debug.log`
|
||||
|
||||
3. Tail logs in real-time:
|
||||
```bash
|
||||
tail -f wp-content/debug.log
|
||||
```
|
||||
|
||||
## Test Procedure
|
||||
|
||||
### Test Case 1: Publish Content with Full Metadata
|
||||
1. Create content in IGNY8 with:
|
||||
- Title: "Test Content Dec 1"
|
||||
- Content HTML: Full article body
|
||||
- ContentTaxonomyMap: Link to taxonomy term "Marketing"
|
||||
- Primary Keyword: "seo strategy"
|
||||
- Secondary Keywords: ["digital marketing", "content marketing"]
|
||||
- Images: 1 featured, 2 gallery
|
||||
|
||||
2. Click Publish button
|
||||
|
||||
3. Check IGNY8 logs for:
|
||||
- ✅ Categories extracted: Should see "Marketing"
|
||||
- ✅ Tags extracted: Should see "seo strategy", "digital marketing", "content marketing"
|
||||
- ✅ Images extracted: Should see featured + 2 gallery
|
||||
- ✅ Status changed to 'published'
|
||||
|
||||
4. Check WordPress logs for:
|
||||
- ✅ Received categories array with "Marketing"
|
||||
- ✅ Received tags array with 3 items
|
||||
- ✅ Received featured_image_url
|
||||
- ✅ Received gallery_images array with 2 items
|
||||
- ✅ Post created with ID
|
||||
- ✅ Categories assigned
|
||||
- ✅ Tags assigned
|
||||
- ✅ Images downloaded and attached
|
||||
|
||||
5. Check WordPress admin:
|
||||
- Go to Posts → All Posts
|
||||
- Find the post "Test Content Dec 1"
|
||||
- Open it for editing
|
||||
- Verify:
|
||||
- ✅ Categories: "Marketing" is checked
|
||||
- ✅ Tags: "seo strategy", "digital marketing", "content marketing" appear
|
||||
- ✅ Featured image is set
|
||||
- ✅ Gallery images are in media library
|
||||
|
||||
### Test Case 2: Check Status Sync
|
||||
1. Publish content from IGNY8
|
||||
2. Immediately check IGNY8 app → Published page
|
||||
3. ✅ Content should appear with status "Published"
|
||||
4. Call WordPress status endpoint
|
||||
5. ✅ Should return wordpress_status: "publish"
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Issue: No categories/tags being sent
|
||||
**Diagnosis:**
|
||||
- Check IGNY8 logs for: `Found 0 taxonomy mappings`
|
||||
- Check IGNY8 logs for: `No primary keyword found`
|
||||
|
||||
**Solution:**
|
||||
- Ensure Content has ContentTaxonomyMap entries
|
||||
- Ensure Content has primary_keyword and secondary_keywords populated
|
||||
|
||||
### Issue: Images not appearing
|
||||
**Diagnosis:**
|
||||
- Check IGNY8 logs for: `Found 0 images for content`
|
||||
|
||||
**Solution:**
|
||||
- Ensure Images model has records linked to content
|
||||
- Ensure Images have image_url populated
|
||||
- Ensure Images have correct image_type ('featured' or 'in_article')
|
||||
|
||||
### Issue: WordPress receives empty arrays
|
||||
**Diagnosis:**
|
||||
- WordPress logs show: `Categories: []`, `Tags: []`
|
||||
|
||||
**Solution:**
|
||||
- This means IGNY8 backend is not extracting data from Content model
|
||||
- Check that Content.id matches the one being published
|
||||
- Check that ContentTaxonomyMap.content_id matches Content.id
|
||||
- Check that Images.content_id matches Content.id
|
||||
|
||||
### Issue: Status not updating in IGNY8
|
||||
**Diagnosis:**
|
||||
- IGNY8 logs show status change but app still shows "review"
|
||||
|
||||
**Solution:**
|
||||
- Check if frontend is polling/refreshing after publish
|
||||
- Check if Content.status field is actually being saved
|
||||
- Check database directly: `SELECT id, title, status FROM content_content WHERE id = X;`
|
||||
|
||||
## Database Queries for Debugging
|
||||
|
||||
### Check Content Status
|
||||
```sql
|
||||
SELECT
|
||||
id,
|
||||
title,
|
||||
status,
|
||||
external_id,
|
||||
external_url,
|
||||
primary_keyword,
|
||||
secondary_keywords
|
||||
FROM content_content
|
||||
WHERE id = YOUR_CONTENT_ID;
|
||||
```
|
||||
|
||||
### Check Taxonomy Mappings
|
||||
```sql
|
||||
SELECT
|
||||
ctm.id,
|
||||
ctm.content_id,
|
||||
t.name as taxonomy_name
|
||||
FROM content_contenttaxonomymap ctm
|
||||
JOIN content_taxonomy t ON ctm.taxonomy_id = t.id
|
||||
WHERE ctm.content_id = YOUR_CONTENT_ID;
|
||||
```
|
||||
|
||||
### Check Images
|
||||
```sql
|
||||
SELECT
|
||||
id,
|
||||
content_id,
|
||||
image_type,
|
||||
image_url,
|
||||
position
|
||||
FROM writer_images
|
||||
WHERE content_id = YOUR_CONTENT_ID
|
||||
ORDER BY position;
|
||||
```
|
||||
|
||||
### Check WordPress Post Meta
|
||||
```sql
|
||||
-- In WordPress database
|
||||
SELECT
|
||||
post_id,
|
||||
meta_key,
|
||||
meta_value
|
||||
FROM wp_postmeta
|
||||
WHERE post_id = YOUR_POST_ID
|
||||
AND meta_key LIKE '_igny8_%';
|
||||
```
|
||||
|
||||
### Check WordPress Post Terms
|
||||
```sql
|
||||
-- In WordPress database
|
||||
SELECT
|
||||
tr.object_id as post_id,
|
||||
tt.taxonomy,
|
||||
t.name as term_name
|
||||
FROM wp_term_relationships tr
|
||||
JOIN wp_term_taxonomy tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
|
||||
JOIN wp_terms t ON tt.term_id = t.term_id
|
||||
WHERE tr.object_id = YOUR_POST_ID;
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Test with sample content** following Test Case 1 above
|
||||
2. **Collect all log output** from both IGNY8 and WordPress
|
||||
3. **Share logs** for analysis if issues persist
|
||||
4. **Check database** using queries above to verify data exists
|
||||
|
||||
## Log Locations
|
||||
|
||||
### IGNY8 Backend
|
||||
- Celery worker console output
|
||||
- Docker logs: `docker logs igny8_celery`
|
||||
- Django logs: `igny8/backend/logs/` (if configured)
|
||||
|
||||
### WordPress
|
||||
- `wp-content/debug.log`
|
||||
- Apache/Nginx error logs
|
||||
- PHP error logs
|
||||
|
||||
## Expected Log Flow
|
||||
|
||||
When everything works correctly, you should see this sequence:
|
||||
|
||||
**1. IGNY8 Backend (when Publish clicked):**
|
||||
```
|
||||
[ContentViewSet.publish] Queued Celery task abc-123 for content 456, status set to 'published'
|
||||
[publish_content_to_wordpress] 🎯 Celery task started: content_id=456
|
||||
[publish_content_to_wordpress] 📄 Content loaded: title='Test Article'
|
||||
[publish_content_to_wordpress] Found 2 taxonomy mappings
|
||||
[publish_content_to_wordpress] 📁 Added category: 'Marketing'
|
||||
[publish_content_to_wordpress] 📁 Added category: 'Technology'
|
||||
[publish_content_to_wordpress] Found 3 images for content
|
||||
[publish_content_to_wordpress] 🖼️ Featured image: https://...
|
||||
[publish_content_to_wordpress] 🖼️ Gallery image #1: https://...
|
||||
[publish_content_to_wordpress] 🖼️ Gallery image #2: https://...
|
||||
[publish_content_to_wordpress] 🏷️ Primary keyword (tag): 'seo strategy'
|
||||
[publish_content_to_wordpress] 🏷️ Added 2 secondary keywords as tags
|
||||
[publish_content_to_wordpress] 📊 TOTAL: 2 categories, 3 tags
|
||||
[publish_content_to_wordpress] 🚀 POSTing to WordPress: https://site.com/wp-json/...
|
||||
[publish_content_to_wordpress] 📦 Payload summary:
|
||||
- Categories: ['Marketing', 'Technology']
|
||||
- Tags: ['seo strategy', 'digital marketing', 'content marketing']
|
||||
- Featured image: Yes
|
||||
- Gallery images: 2
|
||||
[publish_content_to_wordpress] 📬 WordPress response: status=201
|
||||
[publish_content_to_wordpress] ✅ WordPress post created successfully: post_id=789
|
||||
[publish_content_to_wordpress] 💾 Content model updated:
|
||||
- Status: 'published' → 'published'
|
||||
- External ID: 789
|
||||
- External URL: https://site.com/test-article/
|
||||
[publish_content_to_wordpress] 🎉 Successfully published content 456 to WordPress post 789
|
||||
```
|
||||
|
||||
**2. WordPress (receiving publish request):**
|
||||
```
|
||||
========== IGNY8 PUBLISH REQUEST ==========
|
||||
Content ID: 456
|
||||
Task ID: 123
|
||||
Title: Test Article
|
||||
Content HTML: 5234 chars
|
||||
Categories: ["Marketing","Technology"]
|
||||
Tags: ["seo strategy","digital marketing","content marketing"]
|
||||
Featured Image: https://...
|
||||
Gallery Images: 2 images
|
||||
SEO Title: YES
|
||||
SEO Description: YES
|
||||
Primary Keyword: seo strategy
|
||||
===========================================
|
||||
|
||||
========== IGNY8 CREATE WP POST ==========
|
||||
Content ID: 456
|
||||
Task ID: 123
|
||||
Title: Test Article
|
||||
IGNY8: Processing 2 categories
|
||||
IGNY8: ✅ Assigned 2 categories to post 789
|
||||
IGNY8: Processing 3 tags
|
||||
IGNY8: ✅ Assigned 3 tags to post 789
|
||||
IGNY8: Setting featured image from featured_image_url field: https://...
|
||||
IGNY8: Setting gallery with 2 images
|
||||
IGNY8: Setting SEO meta title: Test Article - SEO Title
|
||||
IGNY8: Setting SEO meta description
|
||||
========== IGNY8 POST CREATION COMPLETE: Post ID 789 ==========
|
||||
```
|
||||
|
||||
If you don't see these logs, something is broken in the flow.
|
||||
|
||||
---
|
||||
|
||||
**Created:** December 1, 2025
|
||||
**Purpose:** Diagnose why fixes didn't work and provide step-by-step debugging
|
||||
@@ -1,518 +0,0 @@
|
||||
# Fixes Applied - November 30, 2025
|
||||
|
||||
## Summary
|
||||
|
||||
Fixed 4 critical issues related to WordPress integration:
|
||||
|
||||
1. ✅ **Status not updating from 'review' to 'published'** - Fixed with optimistic status update
|
||||
2. ✅ **Missing WP Status column on Published page** - Added WordPress status endpoint
|
||||
3. ✅ **Custom taxonomy/attribute columns** - Removed, now using native WP taxonomies
|
||||
4. ✅ **Tags, categories, images, keywords not saving** - Now properly extracted and sent to WordPress
|
||||
|
||||
---
|
||||
|
||||
## Issue 1: Status Not Updating to 'Published'
|
||||
|
||||
### Problem
|
||||
When content was published from the Review page, the status in IGNY8 remained as 'review' even after successful WordPress publication.
|
||||
|
||||
### Root Cause
|
||||
The publish endpoint was queuing a Celery task but not updating the status immediately. Users had to wait for the background task to complete before seeing status change.
|
||||
|
||||
### Fix Applied
|
||||
|
||||
**File:** `e:\Projects\...\igny8\backend\igny8_core\modules\writer\views.py`
|
||||
**Method:** `ContentViewSet.publish()`
|
||||
|
||||
**Changes:**
|
||||
```python
|
||||
# OPTIMISTIC UPDATE: Set status to published immediately for better UX
|
||||
# The Celery task will update external_id and external_url when WordPress responds
|
||||
content.status = 'published'
|
||||
content.save(update_fields=['status', 'updated_at'])
|
||||
|
||||
# Queue publishing task
|
||||
result = publish_content_to_wordpress.delay(
|
||||
content_id=content.id,
|
||||
site_integration_id=site_integration.id
|
||||
)
|
||||
|
||||
# Return with status='published' immediately
|
||||
return success_response(
|
||||
data={
|
||||
'content_id': content.id,
|
||||
'task_id': result.id,
|
||||
'status': 'published', # ← Now returns 'published' immediately
|
||||
'message': 'Publishing queued - content will be published to WordPress shortly'
|
||||
},
|
||||
message='Content status updated to published and queued for WordPress',
|
||||
request=request,
|
||||
status_code=status.HTTP_202_ACCEPTED
|
||||
)
|
||||
```
|
||||
|
||||
**Error Handling:**
|
||||
- If the Celery task fails to queue, status is reverted to 'review'
|
||||
- The background task still sets `external_id`, `external_url`, and confirms status after WordPress responds
|
||||
|
||||
**Result:** Users now see status change to 'published' immediately when clicking Publish button
|
||||
|
||||
---
|
||||
|
||||
## Issue 2: No WP Status Column on Published Page
|
||||
|
||||
### Problem
|
||||
The Published page in IGNY8 didn't show the current WordPress status of published content.
|
||||
|
||||
### Fix Applied
|
||||
|
||||
**File:** `e:\Projects\...\igny8\backend\igny8_core\modules\writer\views.py`
|
||||
**New Endpoint:** `GET /api/v1/writer/content/{id}/wordpress_status/`
|
||||
|
||||
```python
|
||||
@action(detail=True, methods=['get'], url_path='wordpress_status', url_name='wordpress_status')
|
||||
def wordpress_status(self, request, pk=None):
|
||||
"""
|
||||
Get WordPress post status for published content.
|
||||
Calls WordPress REST API to get current status.
|
||||
|
||||
Returns: {
|
||||
'wordpress_status': 'publish'|'draft'|'pending'|null,
|
||||
'external_id': 123,
|
||||
'external_url': 'https://...',
|
||||
'post_title': '...',
|
||||
'post_modified': '2025-11-30...',
|
||||
'last_checked': '2025-11-30T...'
|
||||
}
|
||||
"""
|
||||
```
|
||||
|
||||
**WordPress Plugin Endpoint:** `GET /wp-json/igny8/v1/post-status/{id}/`
|
||||
|
||||
**File:** `c:\Users\Hp\vscode\igny8-wp-integration\includes\class-igny8-rest-api.php`
|
||||
**Updated Method:** `get_post_status()`
|
||||
|
||||
```php
|
||||
/**
|
||||
* Get post status by post ID or content_id
|
||||
* Accepts either WordPress post_id or IGNY8 content_id
|
||||
*/
|
||||
public function get_post_status($request) {
|
||||
$id = intval($request['id']);
|
||||
|
||||
// First try as WordPress post ID
|
||||
$post = get_post($id);
|
||||
|
||||
// If not found, try as IGNY8 content_id
|
||||
if (!$post) {
|
||||
$posts = get_posts(array(
|
||||
'meta_key' => '_igny8_content_id',
|
||||
'meta_value' => $id,
|
||||
'post_type' => 'any',
|
||||
'posts_per_page' => 1,
|
||||
'post_status' => 'any'
|
||||
));
|
||||
$post = !empty($posts) ? $posts[0] : null;
|
||||
}
|
||||
|
||||
return rest_ensure_response(array(
|
||||
'success' => true,
|
||||
'data' => array(
|
||||
'post_id' => $post->ID,
|
||||
'post_status' => $post->post_status, // WordPress status
|
||||
'post_title' => $post->post_title,
|
||||
'post_modified' => $post->post_modified,
|
||||
'wordpress_status' => $post->post_status,
|
||||
'igny8_status' => igny8_map_wp_status_to_igny8($post->post_status),
|
||||
// ... more fields
|
||||
)
|
||||
));
|
||||
}
|
||||
```
|
||||
|
||||
**Frontend Integration:**
|
||||
|
||||
To display WP Status column on Published page, the frontend should:
|
||||
|
||||
1. Call `GET /api/v1/writer/content/{id}/wordpress_status/` for each published content
|
||||
2. Display `wordpress_status` field (e.g., "Published", "Draft", "Pending")
|
||||
3. Optionally show `post_modified` to indicate last update time
|
||||
|
||||
**Status Mapping:**
|
||||
|
||||
| WordPress Status | IGNY8 Status | Display |
|
||||
|-----------------|-------------|---------|
|
||||
| `publish` | `completed` | Published |
|
||||
| `draft` | `draft` | Draft |
|
||||
| `pending` | `pending` | Pending Review |
|
||||
| `private` | `completed` | Private |
|
||||
| `trash` | `archived` | Trashed |
|
||||
| `future` | `scheduled` | Scheduled |
|
||||
|
||||
**Result:** IGNY8 app can now poll WordPress status and display it in the Published page table
|
||||
|
||||
---
|
||||
|
||||
## Issue 3: Remove Custom Taxonomy/Attribute Columns
|
||||
|
||||
### Problem
|
||||
WordPress admin had custom "Taxonomy" and "Attribute" columns that referenced deprecated custom taxonomies instead of using native WordPress categories and tags.
|
||||
|
||||
### Fix Applied
|
||||
|
||||
**File:** `c:\Users\Hp\vscode\igny8-wp-integration\admin\class-admin-columns.php`
|
||||
|
||||
**Removed:**
|
||||
- `render_taxonomy_column()` method
|
||||
- `render_attribute_column()` method
|
||||
- Custom column registration for `igny8_taxonomy` and `igny8_attribute`
|
||||
|
||||
**Before:**
|
||||
```php
|
||||
public function add_columns($columns) {
|
||||
$new_columns = array();
|
||||
foreach ($columns as $key => $value) {
|
||||
$new_columns[$key] = $value;
|
||||
if ($key === 'title') {
|
||||
$new_columns['igny8_taxonomy'] = __('Taxonomy', 'igny8-bridge');
|
||||
$new_columns['igny8_attribute'] = __('Attribute', 'igny8-bridge');
|
||||
}
|
||||
}
|
||||
return $new_columns;
|
||||
}
|
||||
```
|
||||
|
||||
**After:**
|
||||
```php
|
||||
public function add_columns($columns) {
|
||||
// Removed custom taxonomy and attribute columns
|
||||
// Posts now use native WordPress taxonomies (categories, tags, etc.)
|
||||
return $columns;
|
||||
}
|
||||
```
|
||||
|
||||
**Result:**
|
||||
- Posts, pages, and products now only show native WordPress taxonomy columns
|
||||
- Categories, tags, product categories, etc. are displayed in standard WordPress columns
|
||||
- Cleaner admin UI aligned with WordPress standards
|
||||
|
||||
---
|
||||
|
||||
## Issue 4: Tags, Categories, Images, Keywords Not Saving
|
||||
|
||||
### Problem
|
||||
When publishing content from IGNY8 to WordPress:
|
||||
- Tags were not being saved
|
||||
- Categories were not being saved
|
||||
- Featured and gallery images were not being attached
|
||||
- Keywords (primary and secondary) were not being added as tags
|
||||
|
||||
### Root Cause
|
||||
The `publish_content_to_wordpress` Celery task was sending empty arrays:
|
||||
|
||||
```python
|
||||
# BEFORE (BROKEN):
|
||||
content_data = {
|
||||
'title': content.title,
|
||||
'content_html': content.content_html,
|
||||
# ...
|
||||
'featured_image_url': None, # ← Empty
|
||||
'sectors': [], # ← Empty
|
||||
'clusters': [], # ← Empty
|
||||
'tags': [] # ← Empty
|
||||
}
|
||||
```
|
||||
|
||||
### Fix Applied
|
||||
|
||||
**File:** `e:\Projects\...\igny8\backend\igny8_core\tasks\wordpress_publishing.py`
|
||||
**Function:** `publish_content_to_wordpress()`
|
||||
|
||||
**Changes:**
|
||||
|
||||
1. **Extract taxonomy terms from ContentTaxonomyMap:**
|
||||
```python
|
||||
from igny8_core.business.content.models import ContentTaxonomyMap
|
||||
taxonomy_maps = ContentTaxonomyMap.objects.filter(content=content).select_related('taxonomy')
|
||||
|
||||
categories = []
|
||||
tags = []
|
||||
for mapping in taxonomy_maps:
|
||||
tax = mapping.taxonomy
|
||||
if tax:
|
||||
# Add taxonomy term name to categories
|
||||
categories.append(tax.name)
|
||||
```
|
||||
|
||||
2. **Extract images from Images model:**
|
||||
```python
|
||||
from igny8_core.modules.writer.models import Images
|
||||
featured_image_url = None
|
||||
gallery_images = []
|
||||
|
||||
images = Images.objects.filter(content=content).order_by('position')
|
||||
for image in images:
|
||||
if image.image_type == 'featured' and image.image_url:
|
||||
featured_image_url = image.image_url
|
||||
elif image.image_type == 'in_article' and image.image_url:
|
||||
gallery_images.append({
|
||||
'url': image.image_url,
|
||||
'alt': image.alt_text or '',
|
||||
'position': image.position
|
||||
})
|
||||
```
|
||||
|
||||
3. **Add keywords as tags:**
|
||||
```python
|
||||
# Add primary and secondary keywords as tags
|
||||
if content.primary_keyword:
|
||||
tags.append(content.primary_keyword)
|
||||
|
||||
if content.secondary_keywords:
|
||||
if isinstance(content.secondary_keywords, list):
|
||||
tags.extend(content.secondary_keywords)
|
||||
elif isinstance(content.secondary_keywords, str):
|
||||
import json
|
||||
try:
|
||||
keywords = json.loads(content.secondary_keywords)
|
||||
if isinstance(keywords, list):
|
||||
tags.extend(keywords)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
```
|
||||
|
||||
4. **Send complete payload to WordPress:**
|
||||
```python
|
||||
content_data = {
|
||||
'content_id': content.id,
|
||||
'task_id': task_id,
|
||||
'title': content.title,
|
||||
'content_html': content.content_html or '',
|
||||
# ... SEO fields ...
|
||||
'featured_image_url': featured_image_url, # ✅ Now populated
|
||||
'gallery_images': gallery_images, # ✅ Now populated
|
||||
'categories': categories, # ✅ Now populated from taxonomy mappings
|
||||
'tags': tags, # ✅ Now populated from keywords
|
||||
# ...
|
||||
}
|
||||
```
|
||||
|
||||
### How WordPress Processes This Data
|
||||
|
||||
**File:** `c:\Users\Hp\vscode\igny8-wp-integration\sync\igny8-to-wp.php`
|
||||
**Function:** `igny8_create_wordpress_post_from_task()`
|
||||
|
||||
1. **Categories:**
|
||||
```php
|
||||
// Handle categories
|
||||
if (!empty($content_data['categories'])) {
|
||||
$category_ids = igny8_process_categories($content_data['categories'], $post_id);
|
||||
if (!empty($category_ids)) {
|
||||
wp_set_post_terms($post_id, $category_ids, 'category');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Tags:**
|
||||
```php
|
||||
// Handle tags
|
||||
if (!empty($content_data['tags'])) {
|
||||
$tag_ids = igny8_process_tags($content_data['tags'], $post_id);
|
||||
if (!empty($tag_ids)) {
|
||||
wp_set_post_terms($post_id, $tag_ids, 'post_tag');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Featured Image:**
|
||||
```php
|
||||
if (!empty($content_data['featured_image_url'])) {
|
||||
igny8_set_featured_image($post_id, $content_data['featured_image_url']);
|
||||
}
|
||||
```
|
||||
|
||||
4. **Gallery Images:**
|
||||
```php
|
||||
if (!empty($content_data['gallery_images'])) {
|
||||
igny8_set_image_gallery($post_id, $content_data['gallery_images']);
|
||||
}
|
||||
```
|
||||
|
||||
5. **Keywords (stored as post meta):**
|
||||
```php
|
||||
if (!empty($content_data['primary_keyword'])) {
|
||||
update_post_meta($post_id, '_igny8_primary_keyword', $content_data['primary_keyword']);
|
||||
}
|
||||
|
||||
if (!empty($content_data['secondary_keywords'])) {
|
||||
update_post_meta($post_id, '_igny8_secondary_keywords', $content_data['secondary_keywords']);
|
||||
}
|
||||
```
|
||||
|
||||
**Result:**
|
||||
- ✅ Categories created/assigned from taxonomy mappings
|
||||
- ✅ Tags created/assigned from keywords (primary + secondary)
|
||||
- ✅ Featured image downloaded and set as post thumbnail
|
||||
- ✅ Gallery images downloaded and attached to post
|
||||
- ✅ Keywords stored in post meta for SEO plugins
|
||||
|
||||
---
|
||||
|
||||
## Data Flow (Complete)
|
||||
|
||||
### Before Fixes ❌
|
||||
```
|
||||
IGNY8 Content Model
|
||||
├─ title ✓
|
||||
├─ content_html ✓
|
||||
├─ taxonomy_terms → NOT SENT ❌
|
||||
├─ images → NOT SENT ❌
|
||||
├─ keywords → NOT SENT ❌
|
||||
└─ status stays 'review' ❌
|
||||
↓
|
||||
WordPress Post
|
||||
├─ Title + Content ✓
|
||||
├─ No categories ❌
|
||||
├─ No tags ❌
|
||||
├─ No images ❌
|
||||
└─ IGNY8 status still 'review' ❌
|
||||
```
|
||||
|
||||
### After Fixes ✅
|
||||
```
|
||||
IGNY8 Content Model
|
||||
├─ title ✓
|
||||
├─ content_html ✓
|
||||
├─ ContentTaxonomyMap → categories[] ✓
|
||||
├─ Images (featured + gallery) → image URLs ✓
|
||||
├─ primary_keyword + secondary_keywords → tags[] ✓
|
||||
└─ status = 'published' immediately ✓
|
||||
↓
|
||||
WordPress Post
|
||||
├─ Title + Content ✓
|
||||
├─ Categories (from taxonomy terms) ✓
|
||||
├─ Tags (from keywords) ✓
|
||||
├─ Featured Image + Gallery ✓
|
||||
└─ IGNY8 can query WordPress status ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### 1. Test Status Update
|
||||
- [ ] Create content in IGNY8 with status 'review'
|
||||
- [ ] Click "Publish" button
|
||||
- [ ] ✅ Verify status changes to 'published' immediately (not waiting for background task)
|
||||
- [ ] ✅ Verify content appears in WordPress with full content
|
||||
- [ ] ✅ Check IGNY8 `external_id` and `external_url` populated after Celery task completes
|
||||
|
||||
### 2. Test WordPress Status Endpoint
|
||||
- [ ] Publish content from IGNY8
|
||||
- [ ] Call `GET /api/v1/writer/content/{id}/wordpress_status/`
|
||||
- [ ] ✅ Verify response contains `wordpress_status: 'publish'`
|
||||
- [ ] Change post status in WordPress to 'draft'
|
||||
- [ ] Call endpoint again
|
||||
- [ ] ✅ Verify response contains `wordpress_status: 'draft'`
|
||||
|
||||
### 3. Test Custom Columns Removed
|
||||
- [ ] Go to WordPress admin → Posts → All Posts
|
||||
- [ ] ✅ Verify no "Taxonomy" or "Attribute" columns appear
|
||||
- [ ] ✅ Verify only native WP columns (Title, Author, Categories, Tags, Date) are shown
|
||||
|
||||
### 4. Test Tags, Categories, Images
|
||||
- [ ] Create content in IGNY8 with:
|
||||
- `primary_keyword`: "SEO Strategy"
|
||||
- `secondary_keywords`: ["Digital Marketing", "Content Marketing"]
|
||||
- ContentTaxonomyMap: link to taxonomy term "Marketing"
|
||||
- Images: 1 featured image, 2 gallery images
|
||||
- [ ] Publish to WordPress
|
||||
- [ ] In WordPress admin, check the post:
|
||||
- [ ] ✅ Categories: "Marketing" exists
|
||||
- [ ] ✅ Tags: "SEO Strategy", "Digital Marketing", "Content Marketing" exist
|
||||
- [ ] ✅ Featured image is set
|
||||
- [ ] ✅ Gallery images attached to post
|
||||
|
||||
---
|
||||
|
||||
## Files Modified
|
||||
|
||||
### IGNY8 Backend
|
||||
1. `e:\Projects\...\igny8\backend\igny8_core\tasks\wordpress_publishing.py`
|
||||
- Added taxonomy term extraction
|
||||
- Added image extraction from Images model
|
||||
- Added keyword extraction as tags
|
||||
- Populated categories, tags, images in payload
|
||||
|
||||
2. `e:\Projects\...\igny8\backend\igny8_core\modules\writer\views.py`
|
||||
- Updated `ContentViewSet.publish()` to set status='published' immediately (optimistic update)
|
||||
- Added `ContentViewSet.wordpress_status()` endpoint
|
||||
- Added error handling to revert status on failure
|
||||
|
||||
### WordPress Plugin
|
||||
1. `c:\Users\Hp\vscode\igny8-wp-integration\includes\class-igny8-rest-api.php`
|
||||
- Updated `get_post_status()` to accept both WordPress post_id and IGNY8 content_id
|
||||
- Enhanced response with more post metadata
|
||||
|
||||
2. `c:\Users\Hp\vscode\igny8-wp-integration\admin\class-admin-columns.php`
|
||||
- Removed `render_taxonomy_column()` and `render_attribute_column()`
|
||||
- Removed custom taxonomy/attribute column registration
|
||||
- Simplified to use only native WP columns
|
||||
|
||||
---
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
**None** - All changes are backward compatible. The WordPress plugin will still accept old payload formats.
|
||||
|
||||
---
|
||||
|
||||
## Frontend Integration Required
|
||||
|
||||
### Published Page - Add WP Status Column
|
||||
|
||||
The frontend Published page should:
|
||||
|
||||
1. Add "WordPress Status" column to the table
|
||||
2. For each row, call: `GET /api/v1/writer/content/{id}/wordpress_status/`
|
||||
3. Display status with color coding:
|
||||
- `publish` → Green badge "Published"
|
||||
- `draft` → Gray badge "Draft"
|
||||
- `pending` → Yellow badge "Pending"
|
||||
- `trash` → Red badge "Trashed"
|
||||
- `future` → Blue badge "Scheduled"
|
||||
|
||||
4. Optional: Add refresh button to re-check WordPress status
|
||||
|
||||
**Example:**
|
||||
```javascript
|
||||
async function fetchWordPressStatus(contentId) {
|
||||
const response = await fetch(`/api/v1/writer/content/${contentId}/wordpress_status/`);
|
||||
const data = await response.json();
|
||||
return data.data.wordpress_status; // 'publish', 'draft', etc.
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**Status:** ✅ All 4 issues fixed and ready for testing
|
||||
|
||||
**Impact:**
|
||||
- Better UX: Users see immediate status changes
|
||||
- Complete data sync: Tags, categories, images now sync to WordPress
|
||||
- Cleaner admin: Removed confusing custom columns
|
||||
- Monitoring: Can now check WordPress status from IGNY8
|
||||
|
||||
**Next Steps:**
|
||||
1. Test all fixes in staging environment
|
||||
2. Update frontend to use `wordpress_status` endpoint
|
||||
3. Add WP Status column to Published page UI
|
||||
4. Monitor Celery logs for any publishing errors
|
||||
|
||||
---
|
||||
|
||||
**Generated:** November 30, 2025
|
||||
**Priority:** HIGH - Core publishing functionality
|
||||
**Breaking Changes:** None
|
||||
@@ -1,301 +0,0 @@
|
||||
# Fixes Applied - December 1, 2025
|
||||
|
||||
## Issues Fixed
|
||||
|
||||
### 1. ✅ WordPress Debug Log Error (Duplicate Code)
|
||||
**Location:** `c:\Users\Hp\vscode\igny8-wp-integration\sync\igny8-to-wp.php`
|
||||
|
||||
**Problem:** Lines 278-287 contained duplicate code that was executed twice, causing PHP warnings in debug.log:
|
||||
```php
|
||||
error_log('========== IGNY8 POST CREATION COMPLETE: Post ID ' . $post_id . ' =========='); update_post_meta($post_id, '_igny8_meta_title', $content_data['meta_title']);
|
||||
}
|
||||
|
||||
if (!empty($content_data['meta_description'])) {
|
||||
update_post_meta($post_id, '_yoast_wpseo_metadesc', $content_data['meta_description']);
|
||||
// ... duplicate code ...
|
||||
}
|
||||
```
|
||||
|
||||
**Fix Applied:**
|
||||
- Removed duplicate meta_title and meta_description update code (lines 278-287)
|
||||
- Added gallery images handler before final status update
|
||||
- Cleaned up log statement formatting
|
||||
|
||||
**Result:** Clean debug.log output without PHP warnings or duplicate operations
|
||||
|
||||
---
|
||||
|
||||
### 2. ✅ WP Status Column Missing on Published Page
|
||||
**Location:** `e:\Projects\...\igny8\frontend\src\config\pages\published.config.tsx`
|
||||
|
||||
**Problem:** Published page showed "Content Status" (IGNY8 internal status) but not "WP Status" (actual WordPress post status)
|
||||
|
||||
**Fix Applied:**
|
||||
|
||||
#### Frontend Configuration
|
||||
**File:** `published.config.tsx`
|
||||
|
||||
Added new column configuration:
|
||||
```tsx
|
||||
{
|
||||
key: 'wordpress_status',
|
||||
label: 'WP Status',
|
||||
sortable: false,
|
||||
width: '120px',
|
||||
render: (_value: any, row: Content) => {
|
||||
// Check if content has been published to WordPress
|
||||
if (!row.external_id) {
|
||||
return (
|
||||
<Badge color="gray" size="xs" variant="soft">
|
||||
<span className="text-[11px] font-normal">Not Published</span>
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
// WordPress status badge
|
||||
const wpStatus = (row as any).wordpress_status || 'publish';
|
||||
const statusConfig: Record<string, { color: ...; label: string }> = {
|
||||
publish: { color: 'success', label: 'Published' },
|
||||
draft: { color: 'gray', label: 'Draft' },
|
||||
pending: { color: 'amber', label: 'Pending' },
|
||||
future: { color: 'blue', label: 'Scheduled' },
|
||||
private: { color: 'amber', label: 'Private' },
|
||||
trash: { color: 'red', label: 'Trashed' },
|
||||
};
|
||||
|
||||
return <Badge color={config.color}>...</Badge>;
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
#### API Integration
|
||||
**File:** `e:\Projects\...\igny8\frontend\src\services\api.ts`
|
||||
|
||||
1. Updated `Content` interface:
|
||||
```typescript
|
||||
export interface Content {
|
||||
// ... existing fields ...
|
||||
wordpress_status?: 'publish' | 'draft' | 'pending' | 'future' | 'private' | 'trash' | null;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
2. Added WordPress status fetcher:
|
||||
```typescript
|
||||
export interface WordPressStatusResult {
|
||||
wordpress_status: 'publish' | 'draft' | 'pending' | 'future' | 'private' | 'trash' | null;
|
||||
external_id: string | null;
|
||||
external_url: string | null;
|
||||
post_title?: string;
|
||||
post_modified?: string;
|
||||
last_checked?: string;
|
||||
}
|
||||
|
||||
export async function fetchWordPressStatus(contentId: number): Promise<WordPressStatusResult> {
|
||||
try {
|
||||
const response = await fetchAPI(`/v1/writer/content/${contentId}/wordpress_status/`);
|
||||
return response.data || response;
|
||||
} catch (error) {
|
||||
console.warn(`Failed to fetch WordPress status for content ${contentId}:`, error);
|
||||
return {
|
||||
wordpress_status: null,
|
||||
external_id: null,
|
||||
external_url: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Published Page Integration
|
||||
**File:** `e:\Projects\...\igny8\frontend\src\pages\Writer\Published.tsx`
|
||||
|
||||
Updated `loadContent()` to fetch WordPress status:
|
||||
```typescript
|
||||
// Fetch WordPress status for published content
|
||||
const resultsWithWPStatus = await Promise.all(
|
||||
filteredResults.map(async (content) => {
|
||||
if (content.external_id) {
|
||||
try {
|
||||
const wpStatus = await fetchWordPressStatus(content.id);
|
||||
return {
|
||||
...content,
|
||||
wordpress_status: wpStatus.wordpress_status,
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn(`Failed to fetch WP status for content ${content.id}:`, error);
|
||||
return content;
|
||||
}
|
||||
}
|
||||
return content;
|
||||
})
|
||||
);
|
||||
|
||||
setContent(resultsWithWPStatus);
|
||||
```
|
||||
|
||||
**Result:**
|
||||
- ✅ Published page now shows two status columns:
|
||||
- **Content Status**: IGNY8 internal status (draft/published)
|
||||
- **WP Status**: Live WordPress status (Published/Draft/Pending/Scheduled/etc.)
|
||||
- ✅ Status badges are color-coded for quick visual identification
|
||||
- ✅ Status is fetched from WordPress API in real-time when page loads
|
||||
- ✅ Handles error gracefully if WordPress status fetch fails
|
||||
|
||||
---
|
||||
|
||||
## Column Display on Published Page
|
||||
|
||||
The Published page now shows these columns in order:
|
||||
|
||||
1. **Title** - Content title with WordPress link icon
|
||||
2. **Content Status** - IGNY8 status (Draft/Published)
|
||||
3. **WP Status** - WordPress status (Published/Draft/Pending/Scheduled/Trashed/Not Published)
|
||||
4. **Type** - Content type (Post/Page/Product)
|
||||
5. **Structure** - Content structure
|
||||
6. **Cluster** - Content cluster
|
||||
7. **Tags** - Content tags
|
||||
8. **Categories** - Content categories
|
||||
9. **Words** - Word count
|
||||
10. **Created** - Creation date
|
||||
|
||||
---
|
||||
|
||||
## Status Mapping Reference
|
||||
|
||||
### Content Status (IGNY8 Internal)
|
||||
| Status | Badge Color | Meaning |
|
||||
|--------|-------------|---------|
|
||||
| `draft` | Amber | Content is still in draft |
|
||||
| `published` | Green | Content marked as published in IGNY8 |
|
||||
|
||||
### WP Status (WordPress Live Status)
|
||||
| WordPress Status | Badge Color | Display Label | Meaning |
|
||||
|-----------------|-------------|---------------|---------|
|
||||
| `publish` | Green (success) | Published | Live on WordPress |
|
||||
| `draft` | Gray | Draft | Saved as draft in WordPress |
|
||||
| `pending` | Amber | Pending | Awaiting review in WordPress |
|
||||
| `future` | Blue | Scheduled | Scheduled for future publish |
|
||||
| `private` | Amber | Private | Published but private |
|
||||
| `trash` | Red | Trashed | Moved to trash in WordPress |
|
||||
| `null` | Gray | Not Published | Not yet published to WordPress |
|
||||
|
||||
---
|
||||
|
||||
## API Endpoint Used
|
||||
|
||||
**Endpoint:** `GET /api/v1/writer/content/{id}/wordpress_status/`
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"wordpress_status": "publish",
|
||||
"external_id": "123",
|
||||
"external_url": "https://site.com/post-url/",
|
||||
"post_title": "Article Title",
|
||||
"post_modified": "2025-12-01 10:30:00",
|
||||
"last_checked": "2025-12-01T10:35:22Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Backend Implementation:** Already exists in:
|
||||
- `e:\Projects\...\igny8\backend\igny8_core\modules\writer\views.py` (ContentViewSet.wordpress_status())
|
||||
- `c:\Users\Hp\vscode\igny8-wp-integration\includes\class-igny8-rest-api.php` (get_post_status())
|
||||
|
||||
---
|
||||
|
||||
## Testing Instructions
|
||||
|
||||
### Test Case 1: View WP Status for Published Content
|
||||
1. Go to https://app.igny8.com/writer/published
|
||||
2. Look for content with `external_id` (published to WordPress)
|
||||
3. ✅ Should see "WP Status" column with "Published" badge (green)
|
||||
4. Click WordPress link icon to verify post is actually published
|
||||
|
||||
### Test Case 2: View Status for Unpublished Content
|
||||
1. On Published page, look for content without `external_id`
|
||||
2. ✅ Should see "Not Published" badge (gray)
|
||||
3. Click "Publish" button
|
||||
4. ✅ After publish completes, WP Status should update to "Published"
|
||||
|
||||
### Test Case 3: Verify Status Sync with WordPress
|
||||
1. Publish content from IGNY8 → WordPress
|
||||
2. Check Published page → should show "Published" (green)
|
||||
3. Go to WordPress admin → change post status to "Draft"
|
||||
4. Refresh IGNY8 Published page
|
||||
5. ✅ WP Status should update to "Draft" (gray)
|
||||
|
||||
### Test Case 4: Performance Check
|
||||
1. Load Published page with 20+ items
|
||||
2. ✅ Should load within 2-3 seconds (parallel API calls)
|
||||
3. Check browser console for errors
|
||||
4. ✅ Should see no console errors
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
**Parallel Fetching:** WordPress status is fetched in parallel for all content items using `Promise.all()`, so page load time scales well even with many items.
|
||||
|
||||
**Error Handling:** If a single status fetch fails, it doesn't block the entire page - that item just won't show WP status.
|
||||
|
||||
**Caching:** Consider adding client-side caching if users frequently reload the page (future enhancement).
|
||||
|
||||
---
|
||||
|
||||
## Files Modified
|
||||
|
||||
### WordPress Plugin
|
||||
1. `c:\Users\Hp\vscode\igny8-wp-integration\sync\igny8-to-wp.php`
|
||||
- Removed duplicate meta update code (lines 278-287)
|
||||
- Added gallery images handler
|
||||
- Fixed log formatting
|
||||
|
||||
### IGNY8 Frontend
|
||||
2. `e:\Projects\...\igny8\frontend\src\config\pages\published.config.tsx`
|
||||
- Added `wordpress_status` column configuration
|
||||
- Implemented status badge rendering with color coding
|
||||
|
||||
3. `e:\Projects\...\igny8\frontend\src\services\api.ts`
|
||||
- Added `wordpress_status` field to `Content` interface
|
||||
- Created `WordPressStatusResult` interface
|
||||
- Implemented `fetchWordPressStatus()` function
|
||||
|
||||
4. `e:\Projects\...\igny8\frontend\src\pages\Writer\Published.tsx`
|
||||
- Imported `fetchWordPressStatus` function
|
||||
- Updated `loadContent()` to fetch WP status in parallel
|
||||
- Enhanced content array with wordpress_status data
|
||||
|
||||
---
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
**None** - All changes are additive and backward compatible.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Optional Enhancements
|
||||
1. **Add refresh button** - Allow users to manually refresh WP status without reloading page
|
||||
2. **Status indicator** - Show last checked timestamp
|
||||
3. **Bulk status check** - Add "Refresh All WP Status" button
|
||||
4. **Filtering** - Add filter by WP status (show only Published, only Drafts, etc.)
|
||||
5. **Caching** - Cache WP status in frontend state for 5 minutes to reduce API calls
|
||||
|
||||
### Testing Checklist
|
||||
- [x] WordPress debug.log error fixed
|
||||
- [x] WP Status column appears on Published page
|
||||
- [x] Status badges display correct colors
|
||||
- [x] Status reflects actual WordPress post status
|
||||
- [ ] Test with 50+ published items (performance)
|
||||
- [ ] Test error handling when WordPress API is down
|
||||
- [ ] Test status sync after WordPress status change
|
||||
|
||||
---
|
||||
|
||||
**Created:** December 1, 2025
|
||||
**Priority:** HIGH - Critical UX improvement
|
||||
**Status:** ✅ COMPLETE - Ready for testing
|
||||
@@ -1,239 +0,0 @@
|
||||
# Content Publishing Fixes Applied
|
||||
|
||||
**Date:** November 29, 2025
|
||||
**Issue:** Only title was being published to WordPress, not the full content_html
|
||||
**Root Cause:** WordPress REST endpoint was fetching from wrong API endpoint (Tasks model instead of Content model) + Field name mismatches
|
||||
|
||||
---
|
||||
|
||||
## Critical Issue Identified
|
||||
|
||||
**Problem:** WordPress posts were created with only the title, no content body.
|
||||
|
||||
**Root Cause Analysis:**
|
||||
1. WordPress REST endpoint (`class-igny8-rest-api.php`) was making an API callback to `/writer/tasks/{task_id}/`
|
||||
2. This endpoint returns the **Tasks** model, which does NOT have a `content_html` field
|
||||
3. Tasks model only has: `title`, `description`, `keywords` (no actual content)
|
||||
4. Meanwhile, IGNY8 backend was already sending full `content_html` in the POST body
|
||||
5. WordPress was ignoring the POST body and using the API callback response instead
|
||||
|
||||
---
|
||||
|
||||
## Fixes Applied
|
||||
|
||||
### Fix #1: WordPress REST Endpoint (CRITICAL)
|
||||
|
||||
**File:** `includes/class-igny8-rest-api.php`
|
||||
**Function:** `publish_content_to_wordpress()`
|
||||
**Lines Modified:** 460-597
|
||||
|
||||
**What Changed:**
|
||||
- ✅ **REMOVED** 80+ lines of API callback logic (lines 507-545)
|
||||
- ✅ **REMOVED** call to `/writer/tasks/{task_id}/` endpoint
|
||||
- ✅ **CHANGED** to parse POST body directly: `$content_data = $request->get_json_params()`
|
||||
- ✅ **ADDED** validation for required fields: `content_id`, `title`, `content_html`
|
||||
- ✅ **ADDED** debug logging when `IGNY8_DEBUG` flag is defined
|
||||
|
||||
**Before:**
|
||||
```php
|
||||
// WordPress was making a redundant API call
|
||||
$response = $api->get("/writer/tasks/{$task_id}/");
|
||||
$content_data = $response['data'] ?? array(); // ❌ This had NO content_html
|
||||
```
|
||||
|
||||
**After:**
|
||||
```php
|
||||
// WordPress now uses the data IGNY8 already sent
|
||||
$content_data = $request->get_json_params(); // ✅ This has content_html
|
||||
```
|
||||
|
||||
**Impact:** WordPress now receives and uses the full `content_html` field sent by IGNY8 backend.
|
||||
|
||||
---
|
||||
|
||||
### Fix #2: IGNY8 Backend Payload (Field Name Corrections)
|
||||
|
||||
**File:** `backend/igny8_core/tasks/wordpress_publishing.py`
|
||||
**Function:** `publish_content_to_wordpress()`
|
||||
**Lines Modified:** 54-89
|
||||
|
||||
**Field Name Fixes:**
|
||||
|
||||
| ❌ Old (Wrong) | ✅ New (Correct) | Reason |
|
||||
|---|---|---|
|
||||
| `content.brief` | Generate from `content_html` | Content model has no `brief` field |
|
||||
| `content.author.email` | `None` | Content model has no `author` field |
|
||||
| `content.published_at` | `None` | Content model has no `published_at` field |
|
||||
| `getattr(content, 'seo_title', '')` | `content.meta_title or ''` | Correct field is `meta_title` |
|
||||
| `getattr(content, 'seo_description', '')` | `content.meta_description or ''` | Correct field is `meta_description` |
|
||||
| `getattr(content, 'focus_keywords', [])` | `content.secondary_keywords or []` | Correct field is `secondary_keywords` |
|
||||
| `content.featured_image.url` | `None` | Content model has no `featured_image` field |
|
||||
| `content.sectors.all()` | Empty array | Content has `sector` (ForeignKey), not `sectors` (many-to-many) |
|
||||
| `content.clusters.all()` | Empty array | Content has `cluster` (ForeignKey), not `clusters` (many-to-many) |
|
||||
| `getattr(content, 'tags', [])` | Empty array | Content model has no `tags` field |
|
||||
|
||||
**New Fields Added:**
|
||||
- ✅ `primary_keyword`: `content.primary_keyword or ''`
|
||||
- ✅ `cluster_id`: `content.cluster.id if content.cluster else None`
|
||||
- ✅ `sector_id`: `content.sector.id if content.sector else None`
|
||||
|
||||
**Excerpt Generation:**
|
||||
```python
|
||||
# Generate excerpt from content_html (Content model has no 'brief' field)
|
||||
excerpt = ''
|
||||
if content.content_html:
|
||||
from django.utils.html import strip_tags
|
||||
excerpt = strip_tags(content.content_html)[:150].strip()
|
||||
if len(content.content_html) > 150:
|
||||
excerpt += '...'
|
||||
```
|
||||
|
||||
**Impact:** Payload now uses fields that actually exist on Content model, preventing AttributeErrors.
|
||||
|
||||
---
|
||||
|
||||
## Content Model Structure (Reference)
|
||||
|
||||
**File:** `backend/igny8_core/business/content/models.py`
|
||||
**Model:** `Content(SiteSectorBaseModel)`
|
||||
|
||||
### Fields That Exist ✅
|
||||
- `title` (CharField)
|
||||
- `content_html` (TextField) ← **The actual content**
|
||||
- `meta_title` (CharField) ← SEO title
|
||||
- `meta_description` (TextField) ← SEO description
|
||||
- `primary_keyword` (CharField)
|
||||
- `secondary_keywords` (JSONField)
|
||||
- `cluster` (ForeignKey to Clusters)
|
||||
- `content_type` (CharField: post/page/product/taxonomy)
|
||||
- `content_structure` (CharField: article/guide/etc)
|
||||
- `status` (CharField: draft/review/published)
|
||||
- `source` (CharField: igny8/wordpress)
|
||||
- `external_id`, `external_url`, `external_type`, `sync_status`
|
||||
- `created_at`, `updated_at` (from base model)
|
||||
- `account`, `site`, `sector` (from SiteSectorBaseModel)
|
||||
|
||||
### Fields That Do NOT Exist ❌
|
||||
- ❌ `brief` or `excerpt`
|
||||
- ❌ `author`
|
||||
- ❌ `published_at`
|
||||
- ❌ `featured_image`
|
||||
- ❌ `seo_title` (it's `meta_title`)
|
||||
- ❌ `seo_description` (it's `meta_description`)
|
||||
- ❌ `focus_keywords` (it's `secondary_keywords`)
|
||||
- ❌ `sectors` (many-to-many)
|
||||
- ❌ `clusters` (many-to-many)
|
||||
- ❌ `tags`
|
||||
|
||||
---
|
||||
|
||||
## WordPress Function Already Handles content_html Correctly
|
||||
|
||||
**File:** `sync/igny8-to-wp.php`
|
||||
**Function:** `igny8_create_wordpress_post_from_task()`
|
||||
**Lines:** 73-200
|
||||
|
||||
This function was already correctly implemented:
|
||||
|
||||
```php
|
||||
// Stage 1 Schema: accept content_html (new) or content (legacy fallback)
|
||||
$content_html = $content_data['content_html'] ?? $content_data['content'] ?? '';
|
||||
|
||||
// ...
|
||||
|
||||
$post_data = array(
|
||||
'post_title' => sanitize_text_field($content_data['title'] ?? 'Untitled'),
|
||||
'post_content' => wp_kses_post($content_html), // ✅ Uses content_html
|
||||
'post_excerpt' => sanitize_text_field($excerpt),
|
||||
// ...
|
||||
);
|
||||
|
||||
$post_id = wp_insert_post($post_data);
|
||||
```
|
||||
|
||||
**No changes needed** - this function properly extracts `content_html` and creates the WordPress post.
|
||||
|
||||
---
|
||||
|
||||
## Data Flow (Fixed)
|
||||
|
||||
### Before Fix ❌
|
||||
```
|
||||
IGNY8 Backend
|
||||
├─ Sends POST with content_html ✓
|
||||
└─ WordPress receives it ✓
|
||||
├─ Ignores POST body ❌
|
||||
├─ Calls /writer/tasks/{id}/ ❌
|
||||
└─ Gets Tasks model (no content_html) ❌
|
||||
└─ Creates post with only title ❌
|
||||
```
|
||||
|
||||
### After Fix ✅
|
||||
```
|
||||
IGNY8 Backend
|
||||
├─ Sends POST with content_html ✓
|
||||
└─ WordPress receives it ✓
|
||||
├─ Parses POST body ✓
|
||||
├─ Validates content_html present ✓
|
||||
└─ Creates post with full content ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
To verify the fixes work:
|
||||
|
||||
1. ✅ Create a Content object in IGNY8 with full `content_html`
|
||||
2. ✅ Ensure Content has: `title`, `content_html`, `meta_title`, `meta_description`, `cluster`, `sector`
|
||||
3. ✅ Trigger `publish_content_to_wordpress` Celery task
|
||||
4. ✅ Verify WordPress receives full payload with `content_html`
|
||||
5. ✅ Confirm WordPress post created with:
|
||||
- Full content body (not just title)
|
||||
- Correct SEO metadata
|
||||
- Cluster and sector IDs stored
|
||||
6. ✅ Check WordPress postmeta for:
|
||||
- `_igny8_content_id`
|
||||
- `_igny8_task_id`
|
||||
- `_igny8_cluster_id`
|
||||
- `_igny8_sector_id`
|
||||
|
||||
---
|
||||
|
||||
## Debug Logging
|
||||
|
||||
To enable verbose logging, add to WordPress `wp-config.php`:
|
||||
|
||||
```php
|
||||
define('IGNY8_DEBUG', true);
|
||||
define('WP_DEBUG', true);
|
||||
define('WP_DEBUG_LOG', true);
|
||||
```
|
||||
|
||||
This will log:
|
||||
- Content ID received
|
||||
- Title received
|
||||
- Content HTML length
|
||||
- All REST API responses
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**Files Modified:**
|
||||
1. `includes/class-igny8-rest-api.php` - WordPress REST endpoint
|
||||
2. `backend/igny8_core/tasks/wordpress_publishing.py` - IGNY8 backend payload
|
||||
|
||||
**Core Changes:**
|
||||
1. WordPress now uses POST body data instead of making redundant API call
|
||||
2. IGNY8 backend uses correct Content model field names
|
||||
3. Excerpt generated from content_html automatically
|
||||
4. Cluster and sector sent as IDs, not arrays
|
||||
|
||||
**Result:** Full content (including HTML body) now publishes to WordPress correctly.
|
||||
|
||||
---
|
||||
|
||||
**Generated:** 2025-11-29
|
||||
**Status:** FIXES APPLIED - Ready for testing
|
||||
**Priority:** HIGH - Core functionality restored
|
||||
@@ -1,301 +0,0 @@
|
||||
# Publishing Failure - Root Cause Analysis & Fixes
|
||||
|
||||
**Date:** November 29, 2025
|
||||
**Issue:** "Failed to publish" notification when trying to publish from Review page
|
||||
**Status:** FIXED
|
||||
|
||||
---
|
||||
|
||||
## Root Causes Identified
|
||||
|
||||
### Critical Issue 1: Incorrect Publish Endpoint Architecture
|
||||
|
||||
**Problem:** The IGNY8 backend `publish()` endpoint was using an incompatible publishing approach
|
||||
- **File:** `igny8_core/modules/writer/views.py` (ContentViewSet.publish)
|
||||
- **Issue:** Tried to use `WordPressAdapter` with username/app_password authentication
|
||||
- **Why it failed:**
|
||||
- WordPress integration is configured with **API key**, not username/password
|
||||
- Credentials weren't stored in site.metadata as expected
|
||||
- WordPressAdapter expected sync publishing (blocking), but we need async with Celery
|
||||
|
||||
### Critical Issue 2: Broken Celery Task
|
||||
|
||||
**Problem:** The Celery task was trying to import from non-existent model
|
||||
- **File:** `igny8_core/tasks/wordpress_publishing.py`
|
||||
- **Root Cause:**
|
||||
```python
|
||||
from igny8_core.models import ContentPost, SiteIntegration # ❌ igny8_core/models.py doesn't exist!
|
||||
```
|
||||
- **Referenced non-existent fields:**
|
||||
- `ContentPost` model doesn't exist (should be `Content`)
|
||||
- `wordpress_sync_status` field doesn't exist
|
||||
- `wordpress_post_id` field doesn't exist
|
||||
- `wordpress_sync_attempts` field doesn't exist
|
||||
- `last_wordpress_sync` field doesn't exist
|
||||
|
||||
### Critical Issue 3: Field Name Mismatches
|
||||
|
||||
**Problem:** Task was looking for fields on Content model that don't exist
|
||||
- `content.wordpress_sync_status` → ❌ Doesn't exist
|
||||
- `content.wordpress_post_id` → ❌ Doesn't exist
|
||||
- Correct field: `content.external_id`
|
||||
|
||||
---
|
||||
|
||||
## Fixes Applied
|
||||
|
||||
### Fix #1: Redesigned Publish Endpoint
|
||||
|
||||
**File:** `igny8_core/modules/writer/views.py`
|
||||
**Function:** `ContentViewSet.publish()`
|
||||
**Lines:** 760-830
|
||||
|
||||
**What Changed:**
|
||||
- ✅ **REMOVED** the `WordPressAdapter` approach entirely
|
||||
- ✅ **REMOVED** username/app_password lookup from site.metadata
|
||||
- ✅ **CHANGED** to use `SiteIntegration` model (which has API key)
|
||||
- ✅ **CHANGED** to queue a Celery task instead of sync publishing
|
||||
- ✅ **ADDED** automatic integration detection by site and platform
|
||||
|
||||
**Before (Broken):**
|
||||
```python
|
||||
# Wrong approach - sync publishing with wrong credentials
|
||||
from igny8_core.business.publishing.services.adapters.wordpress_adapter import WordPressAdapter
|
||||
|
||||
wp_credentials = site.metadata.get('wordpress', {}) # ❌ Not stored here
|
||||
wp_username = wp_credentials.get('username') # ❌ These fields don't exist
|
||||
wp_app_password = wp_credentials.get('app_password') # ❌
|
||||
|
||||
adapter = WordPressAdapter()
|
||||
result = adapter.publish(...) # ❌ Sync - blocks while publishing
|
||||
```
|
||||
|
||||
**After (Fixed):**
|
||||
```python
|
||||
# Correct approach - async publishing via Celery
|
||||
from igny8_core.business.integration.models import SiteIntegration
|
||||
from igny8_core.tasks.wordpress_publishing import publish_content_to_wordpress
|
||||
|
||||
# Find WordPress integration for this site
|
||||
site_integration = SiteIntegration.objects.filter(
|
||||
site=content.site,
|
||||
platform='wordpress',
|
||||
is_active=True
|
||||
).first()
|
||||
|
||||
# Queue async task
|
||||
result = publish_content_to_wordpress.delay(
|
||||
content_id=content.id,
|
||||
site_integration_id=site_integration.id
|
||||
)
|
||||
|
||||
# Returns 202 ACCEPTED immediately
|
||||
return success_response(
|
||||
data={
|
||||
'content_id': content.id,
|
||||
'task_id': result.id,
|
||||
'status': 'queued'
|
||||
},
|
||||
status_code=status.HTTP_202_ACCEPTED
|
||||
)
|
||||
```
|
||||
|
||||
### Fix #2: Fixed Celery Task Imports and Field References
|
||||
|
||||
**File:** `igny8_core/tasks/wordpress_publishing.py`
|
||||
**Function:** `publish_content_to_wordpress()`
|
||||
|
||||
**Imports Fixed:**
|
||||
```python
|
||||
# ❌ OLD (Broken)
|
||||
from igny8_core.models import ContentPost, SiteIntegration
|
||||
|
||||
# ✅ NEW (Correct)
|
||||
from igny8_core.business.content.models import Content
|
||||
from igny8_core.business.integration.models import SiteIntegration
|
||||
```
|
||||
|
||||
**Field References Fixed:**
|
||||
|
||||
| Old Field | Status | New Field | Reason |
|
||||
|---|---|---|---|
|
||||
| `content.wordpress_sync_status` | ❌ Doesn't exist | `content.external_id` | Unified Content model uses external_id |
|
||||
| `content.wordpress_post_id` | ❌ Doesn't exist | `content.external_id` | Same as above |
|
||||
| `content.wordpress_post_url` | ❌ Doesn't exist | `content.external_url` | Same as above |
|
||||
| `content.wordpress_sync_attempts` | ❌ Doesn't exist | ✅ Removed | Not needed in unified model |
|
||||
| `content.last_wordpress_sync` | ❌ Doesn't exist | ✅ Removed | Using updated_at instead |
|
||||
| Check: `if content.wordpress_sync_status == 'syncing'` | ❌ Wrong field | ✅ Removed | No syncing status needed |
|
||||
|
||||
**Status Update Logic Fixed:**
|
||||
```python
|
||||
# ✅ NOW: Updates unified Content model fields
|
||||
if response.status_code == 201:
|
||||
content.external_id = wp_data.get('post_id')
|
||||
content.external_url = wp_data.get('post_url')
|
||||
content.status = 'published' # ✅ Set status to published
|
||||
content.save(update_fields=['external_id', 'external_url', 'status'])
|
||||
```
|
||||
|
||||
### Fix #3: Updated Helper Celery Functions
|
||||
|
||||
**Functions Updated:**
|
||||
1. `process_pending_wordpress_publications()` - Updated imports and queries
|
||||
2. `bulk_publish_content_to_wordpress()` - Updated imports and field checks
|
||||
3. `wordpress_status_reconciliation()` - Simplified (was broken)
|
||||
4. `retry_failed_wordpress_publications()` - Simplified (was broken)
|
||||
|
||||
---
|
||||
|
||||
## Complete Publishing Flow (After Fixes)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ IGNY8 Frontend - Content Review Page │
|
||||
│ │
|
||||
│ User clicks "Publish" button │
|
||||
└─────────────────────────┬───────────────────────────────────────┘
|
||||
│ POST /api/v1/writer/content/{id}/publish/
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ IGNY8 Backend - REST Endpoint │
|
||||
│ (ContentViewSet.publish) │
|
||||
│ │
|
||||
│ 1. Get Content object │
|
||||
│ 2. Check if already published (external_id exists) │
|
||||
│ 3. Find WordPress SiteIntegration for this site │
|
||||
│ 4. Queue Celery task: publish_content_to_wordpress │
|
||||
│ 5. Return 202 ACCEPTED immediately ✅ │
|
||||
│ (Frontend shows: "Publishing..." spinner) │
|
||||
└─────────────────────────┬───────────────────────────────────────┘
|
||||
│ Async Celery Task Queue
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Celery Worker - Background Task │
|
||||
│ (publish_content_to_wordpress) │
|
||||
│ │
|
||||
│ 1. Get Content from database (correct model) │
|
||||
│ 2. Get SiteIntegration with API key │
|
||||
│ 3. Prepare payload with content_html │
|
||||
│ 4. POST to WordPress: /wp-json/igny8/v1/publish-content/ │
|
||||
│ 5. Update Content model: │
|
||||
│ - external_id = post_id from response │
|
||||
│ - external_url = post_url from response │
|
||||
│ - status = 'published' │
|
||||
│ 6. Return success ✅ │
|
||||
└─────────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ WordPress Plugin │
|
||||
│ (Receives REST request with full content_html) │
|
||||
│ │
|
||||
│ Creates post with: │
|
||||
│ - Title ✅ │
|
||||
│ - Full HTML content ✅ │
|
||||
│ - SEO metadata ✅ │
|
||||
│ - Cluster/sector IDs ✅ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What Changed from User Perspective
|
||||
|
||||
### Before Fixes ❌
|
||||
```
|
||||
User action: Click "Publish" button
|
||||
IGNY8 Response: "Failed to publish"
|
||||
Result: Nothing happens, content not published
|
||||
|
||||
Cause:
|
||||
- Endpoint tries to find WordPress credentials in wrong location
|
||||
- Celery task crashes trying to import non-existent model
|
||||
- User sees generic error
|
||||
```
|
||||
|
||||
### After Fixes ✅
|
||||
```
|
||||
User action: Click "Publish" button
|
||||
IGNY8 Response: "Publishing..." → "Published successfully"
|
||||
Result: Content published to WordPress with full HTML content
|
||||
|
||||
Flow:
|
||||
1. Endpoint immediately queues task (fast response)
|
||||
2. Celery worker processes in background
|
||||
3. WordPress receives full content_html + metadata
|
||||
4. Post created with complete content
|
||||
5. IGNY8 updates Content model with external_id/external_url
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing the Fix
|
||||
|
||||
### Manual Testing
|
||||
1. Go to IGNY8 Content Review page
|
||||
2. Select content with full HTML content
|
||||
3. Click "Publish" button
|
||||
4. Should see: "Publishing queued - content will be published shortly"
|
||||
5. Check WordPress in 5-10 seconds - post should appear with full content
|
||||
|
||||
### Checklist
|
||||
- ✅ Content publishes without "Failed to publish" error
|
||||
- ✅ WordPress post has full HTML content (not just title)
|
||||
- ✅ WordPress post has SEO metadata
|
||||
- ✅ IGNY8 Content model updated with `external_id` and `external_url`
|
||||
- ✅ Cluster and sector IDs stored in WordPress postmeta
|
||||
|
||||
### Monitoring
|
||||
- Enable `IGNY8_DEBUG = True` in Django settings to see logs
|
||||
- Monitor Celery worker logs for any publish failures
|
||||
- Check WordPress `/wp-json/igny8/v1/publish-content/` endpoint logs
|
||||
|
||||
---
|
||||
|
||||
## Files Modified
|
||||
|
||||
1. **IGNY8 Backend - Writer Views**
|
||||
- File: `igny8_core/modules/writer/views.py`
|
||||
- Function: `ContentViewSet.publish()`
|
||||
- Change: Redesigned to use SiteIntegration + Celery
|
||||
|
||||
2. **IGNY8 Backend - Celery Tasks**
|
||||
- File: `igny8_core/tasks/wordpress_publishing.py`
|
||||
- Changes:
|
||||
- Fixed imports: ContentPost → Content
|
||||
- Fixed field references: wordpress_sync_status → external_id
|
||||
- Updated all Celery functions to use correct model
|
||||
|
||||
---
|
||||
|
||||
## Architecture Alignment
|
||||
|
||||
The fixes align publishing with the designed architecture:
|
||||
|
||||
| Component | Before | After |
|
||||
|---|---|---|
|
||||
| Publishing Method | Sync (blocks) | Async (Celery) ✅ |
|
||||
| Credentials | site.metadata | SiteIntegration ✅ |
|
||||
| Model Import | igny8_core.models (doesn't exist) | igny8_core.business.content.models ✅ |
|
||||
| Field for Post ID | wordpress_post_id (doesn't exist) | external_id ✅ |
|
||||
| Endpoint Response | Error on failure | 202 ACCEPTED immediately ✅ |
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**Root Cause:** Publishing endpoint used wrong architecture and Celery task had broken imports
|
||||
|
||||
**Critical Fixes:**
|
||||
1. ✅ Changed publish endpoint to queue Celery task (async)
|
||||
2. ✅ Fixed Celery task imports (ContentPost → Content)
|
||||
3. ✅ Fixed field references (wordpress_post_id → external_id)
|
||||
4. ✅ Updated all helper functions for unified Content model
|
||||
|
||||
**Result:** Publishing now works correctly with full content_html being sent to WordPress
|
||||
|
||||
---
|
||||
|
||||
**Status:** Ready for testing
|
||||
**Priority:** CRITICAL - Core functionality fixed
|
||||
**Breaking Changes:** None - purely internal fixes
|
||||
@@ -1,830 +0,0 @@
|
||||
# Strategic Analysis & Implementation Plan
|
||||
|
||||
## Current State Assessment
|
||||
|
||||
### What Works Well
|
||||
- **Unidirectional flow**: IGNY8 → WordPress (correct approach)
|
||||
- **Comprehensive data mapping**: All fields documented
|
||||
- **Multiple trigger points**: Manual + scheduled
|
||||
- **API authentication**: Solid security model
|
||||
- **Retry mechanism**: Celery handles failures
|
||||
|
||||
### What's Actually Broken/Missing
|
||||
|
||||
---
|
||||
|
||||
## Critical Gaps (Real Functional Issues)
|
||||
|
||||
### **GAP 1: Incomplete Data Transfer**
|
||||
**Problem**: The audit shows fields are mapped, but doesn't confirm ALL data actually transfers in one atomic operation.
|
||||
|
||||
**Current Risk**:
|
||||
- Featured image might fail → rest of content publishes anyway
|
||||
- Gallery images fail → content published without visuals
|
||||
- SEO meta fails → content has no SEO optimization
|
||||
- Categories fail to create → content published orphaned
|
||||
- Sectors/clusters fail → no IGNY8 relationship tracking
|
||||
|
||||
**What's Missing**:
|
||||
- **Pre-flight validation** before starting publish
|
||||
- **Atomic transaction pattern** (all-or-nothing)
|
||||
- **Dependency chain verification** (e.g., author must exist before publishing)
|
||||
- **Rollback on partial failure**
|
||||
|
||||
---
|
||||
|
||||
### **GAP 2: No Publish Count Tracking Back to IGNY8**
|
||||
**Problem**: You stated requirement #4 isn't implemented anywhere in the audit.
|
||||
|
||||
**What's Missing**:
|
||||
- After successful WordPress publish, WordPress must call IGNY8 API to increment:
|
||||
- `post_publish_count` for posts
|
||||
- `page_publish_count` for pages
|
||||
- `product_publish_count` for products
|
||||
- `taxonomy_sync_count` for categories/tags/sectors/clusters
|
||||
|
||||
**Current State**:
|
||||
- WordPress reports back `assigned_post_id` and `post_url`
|
||||
- WordPress does NOT report back publish counts or content type statistics
|
||||
|
||||
**Impact**: IGNY8 dashboard shows incomplete/wrong statistics
|
||||
|
||||
---
|
||||
|
||||
### **GAP 3: Taxonomy Sync Doesn't Track Changes**
|
||||
**Problem**: You need to track if categories/tags/clusters change in WordPress, but current system doesn't.
|
||||
|
||||
**Current Flow**:
|
||||
1. IGNY8 sends: `categories: ["SEO", "Marketing"]`
|
||||
2. WordPress creates/assigns these
|
||||
3. **If user later adds "Content Strategy" in WordPress** → IGNY8 never knows
|
||||
4. **If user removes "Marketing"** → IGNY8 never knows
|
||||
|
||||
**What's Missing**:
|
||||
- WordPress hook to detect taxonomy changes on IGNY8-managed posts
|
||||
- API call to IGNY8 to update taxonomy associations
|
||||
- Endpoint in IGNY8 to receive taxonomy change notifications
|
||||
|
||||
---
|
||||
|
||||
### **GAP 4: Cluster/Sector/Keyword Changes Not Synced**
|
||||
**Problem**: Similar to taxonomy gap but for IGNY8-specific relationships.
|
||||
|
||||
**Scenario**:
|
||||
- Content published with `cluster_id: 12`
|
||||
- User changes in WordPress to `cluster_id: 15` via custom field
|
||||
- IGNY8 still thinks content belongs to cluster 12
|
||||
- Cluster 12 shows wrong content count
|
||||
- Cluster 15 missing content in its list
|
||||
|
||||
**What's Missing**:
|
||||
- Detection mechanism for meta field changes on `_igny8_cluster_id`, `_igny8_sector_id`, `_igny8_keyword_ids`
|
||||
- Sync back to IGNY8 to update relationships
|
||||
- IGNY8 API endpoints to handle relationship updates
|
||||
|
||||
---
|
||||
|
||||
### **GAP 5: Manual vs Auto-Publish Flow Not Distinguished**
|
||||
**Problem**: Both flows use same code path, but they need different handling.
|
||||
|
||||
**Manual Publish (Button Click)**:
|
||||
- Should publish **immediately**
|
||||
- User expects instant feedback
|
||||
- Should override any scheduling
|
||||
- Should force re-publish if already published
|
||||
|
||||
**Auto-Publish/Schedule**:
|
||||
- Should respect `published_at` timestamp
|
||||
- Should not override manual edits in WordPress
|
||||
- Should skip if already published (idempotent)
|
||||
- Should handle timezone conversions
|
||||
|
||||
**What's Missing**:
|
||||
- `publish_mode` flag in API payload (`manual` vs `scheduled`)
|
||||
- Different retry strategies for each mode
|
||||
- Different status reporting for each mode
|
||||
- Override logic for manual re-publish
|
||||
|
||||
---
|
||||
|
||||
### **GAP 6: No Verification After Publish**
|
||||
**Problem**: WordPress reports "success" but doesn't verify the content is actually viewable/accessible.
|
||||
|
||||
**Failure Scenarios Not Caught**:
|
||||
- Post published but permalink returns 404 (rewrite rules not flushed)
|
||||
- Featured image attached but file doesn't exist (upload failed silently)
|
||||
- Categories created but not assigned (database transaction partial commit)
|
||||
- SEO meta saved but plugin not active (meta stored but not used)
|
||||
|
||||
**What's Missing**:
|
||||
- Post-publish verification step
|
||||
- Check permalink returns 200
|
||||
- Verify featured image URL accessible
|
||||
- Verify taxonomies actually assigned (count > 0)
|
||||
- Report verification results to IGNY8
|
||||
|
||||
---
|
||||
|
||||
### **GAP 7: Schedule Publishing Timezone Issues**
|
||||
**Problem**: IGNY8 sends UTC timestamp, WordPress stores in site timezone, confusion inevitable.
|
||||
|
||||
**Scenario**:
|
||||
- IGNY8 schedules for "2025-12-01 10:00:00 UTC"
|
||||
- WordPress site timezone is "America/New_York" (UTC-5)
|
||||
- WordPress interprets as 10:00 AM New York time
|
||||
- Content publishes 5 hours later than intended
|
||||
|
||||
**What's Missing**:
|
||||
- Explicit timezone handling in payload
|
||||
- Timezone conversion logic in WordPress
|
||||
- Verification that scheduled time matches intent
|
||||
|
||||
---
|
||||
|
||||
### **GAP 8: All-or-Nothing Guarantees Missing**
|
||||
**Problem**: Content can be half-published (post exists but missing images/meta).
|
||||
|
||||
**Current Flow**:
|
||||
```
|
||||
1. wp_insert_post() → Success (post ID 1842)
|
||||
2. Download featured image → FAILS
|
||||
3. Assign categories → Success
|
||||
4. Store SEO meta → Success
|
||||
5. Report success to IGNY8 ✓
|
||||
|
||||
Result: Post published without featured image
|
||||
IGNY8 thinks everything succeeded
|
||||
```
|
||||
|
||||
**What's Missing**:
|
||||
- Transaction wrapper around entire publish operation
|
||||
- Failure detection for each sub-operation
|
||||
- Rollback mechanism if any step fails
|
||||
- Detailed error reporting (which step failed)
|
||||
|
||||
---
|
||||
|
||||
### **GAP 9: No Re-Publish Protection**
|
||||
**Problem**: If publish button clicked twice or Celery task runs twice, content duplicates.
|
||||
|
||||
**Scenario**:
|
||||
1. User clicks "Publish" in IGNY8
|
||||
2. Celery task queued
|
||||
3. User clicks "Publish" again (impatient)
|
||||
4. Second Celery task queued
|
||||
5. Both tasks run → **2 WordPress posts created for same content**
|
||||
|
||||
**What's Missing**:
|
||||
- Task deduplication based on `content_id` + `site_integration_id`
|
||||
- Lock mechanism during publish
|
||||
- WordPress duplicate detection by `_igny8_content_id` before creating new post
|
||||
- Return existing post if already published (idempotent operation)
|
||||
|
||||
---
|
||||
|
||||
### **GAP 10: Publish Count Statistics Incomplete**
|
||||
**Problem**: You need counts by content type, but current system doesn't track this granularly.
|
||||
|
||||
**What IGNY8 Needs**:
|
||||
```python
|
||||
class SiteIntegration(models.Model):
|
||||
# Current (missing):
|
||||
posts_published_count = models.IntegerField(default=0)
|
||||
pages_published_count = models.IntegerField(default=0)
|
||||
products_published_count = models.IntegerField(default=0)
|
||||
|
||||
# Also need:
|
||||
categories_synced_count = models.IntegerField(default=0)
|
||||
tags_synced_count = models.IntegerField(default=0)
|
||||
sectors_synced_count = models.IntegerField(default=0)
|
||||
clusters_synced_count = models.IntegerField(default=0)
|
||||
|
||||
last_publish_at = models.DateTimeField(null=True)
|
||||
total_sync_operations = models.IntegerField(default=0)
|
||||
```
|
||||
|
||||
**What's Missing**:
|
||||
- WordPress needs to detect content type (post/page/product) and report it
|
||||
- WordPress needs to count new vs updated taxonomies and report
|
||||
- IGNY8 needs endpoints to receive these counts
|
||||
- Dashboard needs to display these statistics
|
||||
|
||||
---
|
||||
|
||||
### **GAP 11: Auto-Publish Scheduling Mechanism Unclear**
|
||||
**Problem**: Audit shows Celery runs every 5 minutes, but doesn't explain how scheduled publishing works.
|
||||
|
||||
**Questions Unanswered**:
|
||||
- If `published_at` is in future, does Celery skip it?
|
||||
- How does Celery know when to publish scheduled content?
|
||||
- Is there a separate queue for scheduled vs immediate?
|
||||
- What if scheduled time is missed (server down)?
|
||||
|
||||
**What's Likely Missing**:
|
||||
- Scheduled content query filter in Celery task
|
||||
- Time-based condition: `published_at <= now()`
|
||||
- Missed schedule handler (publish immediately if past due)
|
||||
- Different retry logic for scheduled vs immediate
|
||||
|
||||
---
|
||||
|
||||
### **GAP 12: Taxonomy Creation vs Assignment Not Clear**
|
||||
**Problem**: If category "Digital Marketing" doesn't exist in WordPress, what happens?
|
||||
|
||||
**Scenario 1: Auto-Create** (probably current):
|
||||
- WordPress creates category "Digital Marketing"
|
||||
- Assigns to post
|
||||
- **Problem**: Might create duplicates if slug differs ("digital-marketing" vs "digitalmarketing")
|
||||
|
||||
**Scenario 2: Map to Existing**:
|
||||
- WordPress looks up by name
|
||||
- If not found, uses fallback category
|
||||
- **Problem**: User needs to pre-create all categories
|
||||
|
||||
**What's Missing**:
|
||||
- Clear taxonomy reconciliation strategy
|
||||
- Slug normalization rules
|
||||
- Duplicate prevention logic
|
||||
- Fallback category configuration
|
||||
|
||||
---
|
||||
|
||||
### **GAP 13: Keywords Not Actually Published**
|
||||
**Problem**: Audit shows `focus_keywords` stored in meta, but WordPress doesn't use this field natively.
|
||||
|
||||
**Current State**:
|
||||
- IGNY8 sends: `focus_keywords: ["SEO 2025", "ranking factors"]`
|
||||
- WordPress stores: `_igny8_focus_keywords` meta
|
||||
- **Nobody reads this field** (unless custom code added)
|
||||
|
||||
**What's Missing**:
|
||||
- Integration with actual keyword tracking plugins (Yoast, RankMath, AIOSEO)
|
||||
- Mapping to plugin-specific meta fields
|
||||
- Fallback if no SEO plugin installed
|
||||
|
||||
---
|
||||
|
||||
### **GAP 14: Gallery Images Limit Arbitrary**
|
||||
**Problem**: Audit mentions "5 images max" for gallery but doesn't explain why or what happens to 6th image.
|
||||
|
||||
**Questions**:
|
||||
- Is this IGNY8 limit or WordPress plugin limit?
|
||||
- What happens if IGNY8 sends 10 images?
|
||||
- Are they silently dropped? Error thrown?
|
||||
- How does user know some images were skipped?
|
||||
|
||||
**What's Missing**:
|
||||
- Configurable gallery size limit
|
||||
- Clear error message if limit exceeded
|
||||
- Option to create separate gallery post/page for overflow
|
||||
|
||||
---
|
||||
|
||||
## Implementation Plan (No Code)
|
||||
|
||||
### **Phase 1: Fix Critical Data Integrity Issues** (Week 1-2)
|
||||
|
||||
#### 1.1 Implement Atomic Transaction Pattern
|
||||
- Wrap entire publish operation in WordPress transaction
|
||||
- If ANY step fails → rollback everything
|
||||
- Delete post if created but subsequent operations failed
|
||||
- Report detailed failure info to IGNY8 (which step failed)
|
||||
|
||||
#### 1.2 Add Pre-Flight Validation
|
||||
Before attempting publish:
|
||||
- Verify author exists (by email)
|
||||
- Verify all image URLs accessible (HTTP HEAD request)
|
||||
- Verify required fields present (title, content)
|
||||
- Verify post type enabled in WordPress plugin settings
|
||||
- Return validation errors BEFORE creating anything
|
||||
|
||||
#### 1.3 Implement Duplicate Prevention
|
||||
- Check if post with `_igny8_content_id` already exists
|
||||
- If exists → update instead of create (unless manual re-publish)
|
||||
- Add unique constraint in IGNY8: `(content_id, site_integration_id)` → only one publish task active at a time
|
||||
- Celery task deduplication by task signature
|
||||
|
||||
#### 1.4 Add Post-Publish Verification
|
||||
After WordPress reports "success":
|
||||
- Wait 5 seconds (let WordPress flush rewrites)
|
||||
- HTTP GET the permalink → expect 200
|
||||
- HTTP HEAD the featured image URL → expect 200
|
||||
- Query taxonomies assigned → expect count > 0
|
||||
- If verification fails → mark as "published_with_issues" status
|
||||
- Report verification results to IGNY8
|
||||
|
||||
---
|
||||
|
||||
### **Phase 2: Implement Publish Count Tracking** (Week 2-3)
|
||||
|
||||
#### 2.1 Extend IGNY8 Models
|
||||
Add to `SiteIntegration`:
|
||||
- `posts_published_count`
|
||||
- `pages_published_count`
|
||||
- `products_published_count`
|
||||
- `categories_synced_count`
|
||||
- `tags_synced_count`
|
||||
- `sectors_synced_count`
|
||||
- `clusters_synced_count`
|
||||
- `last_publish_at`
|
||||
- `total_sync_operations`
|
||||
|
||||
#### 2.2 Create IGNY8 Stats Endpoint
|
||||
```
|
||||
PUT /integrations/{site_id}/stats/increment/
|
||||
Payload: {
|
||||
"content_type": "post", // or "page", "product"
|
||||
"taxonomies_created": {
|
||||
"categories": 2,
|
||||
"tags": 5,
|
||||
"sectors": 1,
|
||||
"clusters": 1
|
||||
},
|
||||
"taxonomies_updated": {
|
||||
"categories": 0,
|
||||
"tags": 1,
|
||||
"sectors": 0,
|
||||
"clusters": 0
|
||||
},
|
||||
"published_at": "2025-11-29T10:15:30Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.3 Update WordPress Plugin Response
|
||||
After successful publish, WordPress must:
|
||||
- Detect post type (post/page/product)
|
||||
- Count new categories created vs existing assigned
|
||||
- Count new tags created vs existing assigned
|
||||
- Count new sectors created vs existing assigned
|
||||
- Count new clusters created vs existing assigned
|
||||
- Call IGNY8 stats endpoint with all counts
|
||||
- IGNY8 increments counters atomically
|
||||
|
||||
---
|
||||
|
||||
### **Phase 3: Implement Taxonomy Change Tracking** (Week 3-4)
|
||||
|
||||
#### 3.1 Add WordPress Hooks
|
||||
Hook into:
|
||||
- `set_object_terms` (when taxonomies assigned/changed)
|
||||
- `update_post_meta` (when cluster/sector/keyword meta changed)
|
||||
- Filter by: post has `_igny8_task_id` meta (only track IGNY8-managed posts)
|
||||
|
||||
#### 3.2 Create IGNY8 Taxonomy Update Endpoint
|
||||
```
|
||||
PUT /writer/tasks/{task_id}/taxonomies/
|
||||
Payload: {
|
||||
"categories": [1, 2, 3], // WordPress term IDs
|
||||
"tags": [5, 8, 12],
|
||||
"sectors": [2],
|
||||
"clusters": [7, 9],
|
||||
"updated_by": "wordpress_user_123",
|
||||
"updated_at": "2025-11-29T11:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.3 Create IGNY8 Relationships Update Endpoint
|
||||
```
|
||||
PUT /writer/tasks/{task_id}/relationships/
|
||||
Payload: {
|
||||
"cluster_id": 15, // changed from 12
|
||||
"sector_id": 5, // unchanged
|
||||
"keyword_ids": [1, 2, 3, 8], // added keyword 8
|
||||
"updated_by": "wordpress_user_123",
|
||||
"updated_at": "2025-11-29T11:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.4 Implement Debouncing
|
||||
- Don't sync every single taxonomy change immediately
|
||||
- Batch changes over 30-second window
|
||||
- Send one API call with all changes
|
||||
- Reduce API call volume by 95%
|
||||
|
||||
---
|
||||
|
||||
### **Phase 4: Separate Manual vs Auto-Publish Flows** (Week 4-5)
|
||||
|
||||
#### 4.1 Add `publish_mode` to API Payload
|
||||
IGNY8 must send:
|
||||
```json
|
||||
{
|
||||
"content_id": 42,
|
||||
"publish_mode": "manual", // or "scheduled"
|
||||
"published_at": "2025-12-01T10:00:00Z",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.2 Implement Different Logic
|
||||
|
||||
**Manual Mode**:
|
||||
- Ignore `published_at` timestamp (publish NOW)
|
||||
- If post already exists → force update (don't skip)
|
||||
- Return immediate feedback (synchronous within 5 seconds)
|
||||
- Retry aggressively (3 retries, 10 seconds apart)
|
||||
- Show user real-time progress
|
||||
|
||||
**Scheduled Mode**:
|
||||
- Respect `published_at` timestamp
|
||||
- If post already exists → skip (idempotent)
|
||||
- Queue for future execution
|
||||
- Retry conservatively (3 retries, 1 hour apart)
|
||||
- Don't notify user of each retry
|
||||
|
||||
#### 4.3 Update Celery Task Query
|
||||
```python
|
||||
# Current: publishes everything with status='completed'
|
||||
pending_content = ContentPost.objects.filter(
|
||||
wordpress_sync_status='pending',
|
||||
published_at__isnull=False
|
||||
)
|
||||
|
||||
# New: separate scheduled from immediate
|
||||
immediate_content = ContentPost.objects.filter(
|
||||
wordpress_sync_status='pending',
|
||||
publish_mode='manual',
|
||||
published_at__isnull=False
|
||||
)
|
||||
|
||||
scheduled_content = ContentPost.objects.filter(
|
||||
wordpress_sync_status='pending',
|
||||
publish_mode='scheduled',
|
||||
published_at__lte=now(), # only if scheduled time reached
|
||||
published_at__isnull=False
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Phase 5: Timezone Handling** (Week 5)
|
||||
|
||||
#### 5.1 Standardize on UTC Everywhere
|
||||
- IGNY8 always sends timestamps in UTC with explicit timezone: `"2025-12-01T10:00:00Z"`
|
||||
- WordPress plugin converts to site timezone for `post_date`
|
||||
- WordPress converts back to UTC when reporting to IGNY8
|
||||
- Never rely on implied timezones
|
||||
|
||||
#### 5.2 Add Timezone to WordPress Response
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"post_id": 1842,
|
||||
"post_date_utc": "2025-11-29T10:15:30Z",
|
||||
"post_date_site": "2025-11-29T05:15:30-05:00",
|
||||
"site_timezone": "America/New_York"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 5.3 Scheduled Publish Verification
|
||||
- IGNY8 stores: "Scheduled for 2025-12-01 10:00 UTC"
|
||||
- WordPress publishes at: "2025-12-01 05:00 EST" (correct)
|
||||
- WordPress reports back: "Published at 2025-12-01T10:00:00Z" (UTC)
|
||||
- IGNY8 verifies timestamp matches expected
|
||||
|
||||
---
|
||||
|
||||
### **Phase 6: Enhanced Error Reporting** (Week 6)
|
||||
|
||||
#### 6.1 Add Detailed Error Structure
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "FEATURED_IMAGE_DOWNLOAD_FAILED",
|
||||
"message": "Failed to download featured image",
|
||||
"step": "media_processing",
|
||||
"step_number": 3,
|
||||
"total_steps": 7,
|
||||
"details": {
|
||||
"image_url": "https://example.com/image.jpg",
|
||||
"http_status": 404,
|
||||
"error": "Not Found"
|
||||
},
|
||||
"recoverable": true,
|
||||
"retry_recommended": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 6.2 Add Progress Reporting for Manual Publish
|
||||
For manual publish, send progress updates:
|
||||
```
|
||||
POST /integrations/{site_id}/publish-progress/
|
||||
{
|
||||
"task_id": 15,
|
||||
"step": "creating_post",
|
||||
"progress": 30,
|
||||
"message": "Creating WordPress post..."
|
||||
}
|
||||
```
|
||||
|
||||
Frontend shows real-time progress bar.
|
||||
|
||||
---
|
||||
|
||||
### **Phase 7: Taxonomy Reconciliation Strategy** (Week 6-7)
|
||||
|
||||
#### 7.1 Add Taxonomy Mapping Configuration
|
||||
WordPress plugin settings:
|
||||
- **Auto-create missing taxonomies**: ON/OFF
|
||||
- **Slug normalization**: lowercase + hyphens
|
||||
- **Duplicate detection**: by slug (not name)
|
||||
- **Fallback category**: "Uncategorized" (if auto-create OFF and category not found)
|
||||
|
||||
#### 7.2 Taxonomy Reconciliation Algorithm
|
||||
```
|
||||
For each category in IGNY8 payload:
|
||||
1. Normalize slug: "Digital Marketing" → "digital-marketing"
|
||||
2. Query WordPress by slug (not name)
|
||||
3. If found → use existing term ID
|
||||
4. If not found:
|
||||
a. If auto-create ON → create new term
|
||||
b. If auto-create OFF → use fallback category
|
||||
5. Assign term to post
|
||||
```
|
||||
|
||||
#### 7.3 Report Taxonomy Changes to IGNY8
|
||||
```json
|
||||
{
|
||||
"taxonomies_processed": {
|
||||
"categories": {
|
||||
"requested": ["Digital Marketing", "SEO"],
|
||||
"created": ["SEO"],
|
||||
"existing": ["Digital Marketing"],
|
||||
"assigned": [1, 5]
|
||||
},
|
||||
"tags": {
|
||||
"requested": ["seo", "ranking"],
|
||||
"created": [],
|
||||
"existing": ["seo", "ranking"],
|
||||
"assigned": [8, 12]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Phase 8: SEO Plugin Integration** (Week 7-8)
|
||||
|
||||
#### 8.1 Detect Active SEO Plugin
|
||||
WordPress plugin detects:
|
||||
- Yoast SEO
|
||||
- Rank Math
|
||||
- All in One SEO
|
||||
- SEOPress
|
||||
- (or none)
|
||||
|
||||
#### 8.2 Map Focus Keywords to Plugin Fields
|
||||
|
||||
**Yoast SEO**:
|
||||
- `_yoast_wpseo_focuskw` = first keyword
|
||||
- `_yoast_wpseo_keywordsynonyms` = remaining keywords (comma-separated)
|
||||
|
||||
**Rank Math**:
|
||||
- `rank_math_focus_keyword` = first keyword
|
||||
- Additional keywords stored in JSON meta
|
||||
|
||||
**All in One SEO**:
|
||||
- `_aioseo_keywords` = comma-separated list
|
||||
|
||||
**No Plugin**:
|
||||
- Store in `_igny8_focus_keywords` (current behavior)
|
||||
- Optional: Generate simple meta keywords tag
|
||||
|
||||
#### 8.3 Report SEO Plugin Status to IGNY8
|
||||
```json
|
||||
{
|
||||
"seo_plugin": {
|
||||
"active": "yoast",
|
||||
"version": "22.0",
|
||||
"keywords_supported": true,
|
||||
"focus_keyword_set": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Phase 9: Gallery Image Handling** (Week 8)
|
||||
|
||||
#### 9.1 Make Gallery Limit Configurable
|
||||
WordPress plugin settings:
|
||||
- **Max gallery images**: 5 (default)
|
||||
- **Overflow behavior**:
|
||||
- "Skip extra images" (current)
|
||||
- "Create separate gallery post"
|
||||
- "Add to post content as image grid"
|
||||
|
||||
#### 9.2 Handle Overflow Images
|
||||
If IGNY8 sends 10 images but limit is 5:
|
||||
|
||||
**Option A: Skip**:
|
||||
- Use first 5
|
||||
- Report to IGNY8: `"gallery_images_skipped": 5`
|
||||
|
||||
**Option B: Create Separate Post**:
|
||||
- Create new post: "{Original Title} - Gallery"
|
||||
- Attach images 6-10
|
||||
- Link from original post
|
||||
- Report to IGNY8: `"gallery_overflow_post_id": 1843`
|
||||
|
||||
**Option C: Inline Grid**:
|
||||
- Append HTML grid to post content
|
||||
- All 10 images in post body
|
||||
- Report to IGNY8: `"gallery_images_inline": 10`
|
||||
|
||||
---
|
||||
|
||||
### **Phase 10: Monitoring & Dashboard** (Week 9)
|
||||
|
||||
#### 10.1 IGNY8 Dashboard Enhancements
|
||||
Display per site:
|
||||
- **Total Published**: Posts (X) | Pages (Y) | Products (Z)
|
||||
- **Taxonomies Synced**: Categories (A) | Tags (B) | Sectors (C) | Clusters (D)
|
||||
- **Last Published**: 2 hours ago
|
||||
- **Publish Success Rate**: 98.5% (last 30 days)
|
||||
- **Average Publish Time**: 3.2 seconds
|
||||
- **Pending**: 5 scheduled for today
|
||||
|
||||
#### 10.2 WordPress Plugin Dashboard
|
||||
Display:
|
||||
- **IGNY8 Posts**: 142 published | 5 pending
|
||||
- **Last Sync**: 10 minutes ago
|
||||
- **Connection Status**: Connected ✓
|
||||
- **Recent Activity**:
|
||||
- 10:15 AM - Published "SEO Guide 2025" (post)
|
||||
- 10:05 AM - Published "About Us" (page)
|
||||
- 09:50 AM - Synced 3 categories
|
||||
|
||||
#### 10.3 Add Health Check Endpoint
|
||||
```
|
||||
GET /wp-json/igny8/v1/health
|
||||
Response:
|
||||
{
|
||||
"status": "healthy",
|
||||
"checks": {
|
||||
"api_connection": "ok",
|
||||
"database": "ok",
|
||||
"media_uploads": "ok",
|
||||
"taxonomy_creation": "ok"
|
||||
},
|
||||
"stats": {
|
||||
"posts_managed": 142,
|
||||
"last_publish": "2025-11-29T10:15:30Z",
|
||||
"disk_space": "15GB free"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Call from IGNY8 every 5 minutes to detect issues early.
|
||||
|
||||
---
|
||||
|
||||
## Summary: What Actually Needs to Change
|
||||
|
||||
### **Backend (IGNY8 Django)** Changes:
|
||||
|
||||
1. **Add Models Fields**:
|
||||
- `publish_mode` to ContentPost ('manual' or 'scheduled')
|
||||
- Publish count fields to SiteIntegration
|
||||
- Taxonomy sync count fields
|
||||
|
||||
2. **Add API Endpoints**:
|
||||
- `PUT /integrations/{id}/stats/increment/` (receive counts from WP)
|
||||
- `PUT /writer/tasks/{id}/taxonomies/` (receive taxonomy changes from WP)
|
||||
- `PUT /writer/tasks/{id}/relationships/` (receive cluster/sector changes from WP)
|
||||
|
||||
3. **Update Celery Task**:
|
||||
- Add pre-flight validation
|
||||
- Separate scheduled vs manual queries
|
||||
- Add duplicate prevention
|
||||
- Add timezone handling
|
||||
- Improve error reporting
|
||||
|
||||
4. **Update API Call to WordPress**:
|
||||
- Send `publish_mode` flag
|
||||
- Send explicit UTC timezone
|
||||
- Handle detailed error responses
|
||||
- Process verification results
|
||||
|
||||
---
|
||||
|
||||
### **Frontend (IGNY8 Vue/React)** Changes:
|
||||
|
||||
1. **Manual Publish Button**:
|
||||
- Show real-time progress (if WordPress sends updates)
|
||||
- Show detailed success message with link to WP post
|
||||
- Show detailed error message if fails (which step failed)
|
||||
|
||||
2. **Dashboard Stats**:
|
||||
- Display publish counts by content type
|
||||
- Display taxonomy sync counts
|
||||
- Display last publish timestamp
|
||||
- Display success rate graph
|
||||
|
||||
3. **Scheduled Publish UI**:
|
||||
- Datetime picker with timezone display
|
||||
- "Schedule for: Dec 1, 2025 10:00 AM UTC (5:00 AM your time)"
|
||||
- List of scheduled publications
|
||||
- Ability to cancel scheduled publish
|
||||
|
||||
---
|
||||
|
||||
### **WordPress Plugin** Changes:
|
||||
|
||||
1. **Core Publish Function**:
|
||||
- Wrap in transaction (all-or-nothing)
|
||||
- Add pre-flight validation
|
||||
- Add duplicate detection
|
||||
- Add post-publish verification
|
||||
- Handle `publish_mode` flag differently
|
||||
|
||||
2. **Add Taxonomy Hooks**:
|
||||
- Detect changes to categories/tags/sectors/clusters
|
||||
- Batch changes over 30 seconds
|
||||
- Call IGNY8 API to sync changes
|
||||
|
||||
3. **Add Stats Tracking**:
|
||||
- Count content types published
|
||||
- Count taxonomies created vs assigned
|
||||
- Call IGNY8 stats endpoint after each publish
|
||||
|
||||
4. **Settings Page**:
|
||||
- Taxonomy auto-create ON/OFF
|
||||
- Taxonomy fallback category selector
|
||||
- Gallery image limit (slider: 1-20)
|
||||
- Gallery overflow behavior (dropdown)
|
||||
- SEO plugin integration status
|
||||
|
||||
5. **Response Format**:
|
||||
- Add verification results
|
||||
- Add taxonomy processing details
|
||||
- Add publish counts
|
||||
- Add timezone info
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### 1. **Atomic Transaction Tests**
|
||||
- Publish with invalid image URL → entire operation should fail, no post created
|
||||
- Publish with invalid author → entire operation should fail
|
||||
- Publish with SEO plugin disabled → post created, SEO meta stored anyway
|
||||
|
||||
### 2. **Duplicate Prevention Tests**
|
||||
- Click publish button twice rapidly → only 1 post created
|
||||
- Celery task runs while manual publish in progress → only 1 post created
|
||||
- Re-publish same content → update existing post, don't create new
|
||||
|
||||
### 3. **Timezone Tests**
|
||||
- Schedule for "Dec 1, 2025 10:00 UTC" from timezone UTC+5 → publishes at correct time
|
||||
- WordPress in timezone "America/New_York" → post_date stored correctly in local time
|
||||
- IGNY8 receives post_date_utc → matches scheduled time exactly
|
||||
|
||||
### 4. **Taxonomy Sync Tests**
|
||||
- Add category in WordPress → IGNY8 receives update within 30 seconds
|
||||
- Remove tag in WordPress → IGNY8 receives update
|
||||
- Change cluster via custom field → IGNY8 receives update
|
||||
- Change multiple taxonomies at once → IGNY8 receives 1 batched update
|
||||
|
||||
### 5. **Count Tracking Tests**
|
||||
- Publish 1 post → SiteIntegration.posts_published_count increments by 1
|
||||
- Publish 1 page → SiteIntegration.pages_published_count increments by 1
|
||||
- Create 2 new categories → SiteIntegration.categories_synced_count increments by 2
|
||||
- Update post (no new taxonomies) → counts don't change
|
||||
|
||||
### 6. **Manual vs Scheduled Tests**
|
||||
- Manual publish → immediate execution, ignores published_at
|
||||
- Scheduled publish → waits until published_at time
|
||||
- Manual re-publish of scheduled content → publishes immediately, overrides schedule
|
||||
|
||||
---
|
||||
|
||||
## Implementation Priority
|
||||
|
||||
### **Critical (Do First)**:
|
||||
1. Atomic transactions (Phase 1.1)
|
||||
2. Duplicate prevention (Phase 1.3)
|
||||
3. Publish count tracking (Phase 2)
|
||||
4. Manual vs scheduled separation (Phase 4)
|
||||
|
||||
### **High Priority**:
|
||||
5. Timezone handling (Phase 5)
|
||||
6. Taxonomy change tracking (Phase 3)
|
||||
7. Enhanced error reporting (Phase 6)
|
||||
|
||||
### **Medium Priority**:
|
||||
8. Taxonomy reconciliation (Phase 7)
|
||||
9. SEO plugin integration (Phase 8)
|
||||
10. Gallery improvements (Phase 9)
|
||||
|
||||
### **Low Priority**:
|
||||
11. Dashboard enhancements (Phase 10)
|
||||
|
||||
---
|
||||
|
||||
This plan focuses on **real functional gaps** that affect data integrity, user experience, and system reliability. No cosmetics, just critical infrastructure improvements.
|
||||
@@ -1,356 +0,0 @@
|
||||
# WordPress Plugin ↔ IGNY8 Backend Sync - Data Flow Diagram
|
||||
|
||||
## Complete Sync Journey
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ WORDPRESS ADMIN - Connection Setup │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ User Input: │
|
||||
│ ┌───────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Email: dev@igny8.com │ │
|
||||
│ │ Password: **** │ │
|
||||
│ │ API Key: **** │ │
|
||||
│ └───────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ WORDPRESS PLUGIN - Authentication (class-admin.php handle_connection()) │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 1. POST /auth/login/ (with email + password) │
|
||||
│ ↓ │
|
||||
│ 2. Store: access_token, refresh_token │
|
||||
│ ↓ │
|
||||
│ 3. GET /system/sites/ (authenticated) │
|
||||
│ ↓ │
|
||||
│ 4. Store: site_id (extracted from first site) │
|
||||
│ ↓ │
|
||||
│ ✅ Connection complete! User sees success message │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ WORDPRESS PLUGIN - Gather Site Structure (igny8_sync_site_structure_to_backend)
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Step 1: Query for Integration ID │
|
||||
│ ┌────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ GET /v1/integration/integrations/ │ │
|
||||
│ │ ?site={site_id} │ │
|
||||
│ │ &platform=wordpress ← NEW: Platform filter │ │
|
||||
│ └────────────────────────────────────────────────────────────────┘ │
|
||||
│ ↓ │
|
||||
│ Step 2: Extract Integration ID (handle multiple response formats) │
|
||||
│ ┌────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Response Format Handling: │ │
|
||||
│ │ • Paginated: data.results[0] ← Django REST Framework │ │
|
||||
│ │ • Array: data[0] ← Alternative format │ │
|
||||
│ │ • Object: data ← Direct single object │ │
|
||||
│ └────────────────────────────────────────────────────────────────┘ │
|
||||
│ ↓ │
|
||||
│ Step 3: Gather WordPress Content Structure │
|
||||
│ ┌────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Post Types (igny8_get_site_structure): │ │
|
||||
│ │ ├─ post → "Posts" (count: 50) │ │
|
||||
│ │ ├─ page → "Pages" (count: 10) │ │
|
||||
│ │ └─ product → "Products" (count: 100) │ │
|
||||
│ │ │ │
|
||||
│ │ Taxonomies: │ │
|
||||
│ │ ├─ category → "Categories" (count: 12) │ │
|
||||
│ │ ├─ post_tag → "Tags" (count: 89) │ │
|
||||
│ │ └─ product_cat → "Product Categories" (count: 15) │ │
|
||||
│ │ │ │
|
||||
│ │ Metadata: │ │
|
||||
│ │ ├─ timestamp (ISO 8601 format) ← NEW │ │
|
||||
│ │ ├─ site_url (WordPress domain) ← NEW │ │
|
||||
│ │ └─ wordpress_version (e.g., 6.4) ← NEW │ │
|
||||
│ └────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ With Enhanced Debug Logging (if WP_DEBUG or IGNY8_DEBUG enabled): │
|
||||
│ • Log: Integration ID retrieved │
|
||||
│ • Log: Structure gathered successfully │
|
||||
│ • Log: Ready to sync │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ WORDPRESS → IGNY8 BACKEND - Push Structure (class-igny8-api.php post()) │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ POST /v1/integration/integrations/{integration_id}/update-structure/ │
|
||||
│ │
|
||||
│ Headers: │
|
||||
│ ├─ Authorization: Bearer {access_token} │
|
||||
│ └─ Content-Type: application/json │
|
||||
│ │
|
||||
│ Request Body: │
|
||||
│ ┌────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ { │ │
|
||||
│ │ "post_types": { │ │
|
||||
│ │ "post": { │ │
|
||||
│ │ "label": "Posts", │ │
|
||||
│ │ "count": 50, │ │
|
||||
│ │ "enabled": true, │ │
|
||||
│ │ "fetch_limit": 100 │ │
|
||||
│ │ }, │ │
|
||||
│ │ "page": {...}, │ │
|
||||
│ │ "product": {...} │ │
|
||||
│ │ }, │ │
|
||||
│ │ "taxonomies": { │ │
|
||||
│ │ "category": { │ │
|
||||
│ │ "label": "Categories", │ │
|
||||
│ │ "count": 12, │ │
|
||||
│ │ "enabled": true, │ │
|
||||
│ │ "fetch_limit": 100 │ │
|
||||
│ │ }, │ │
|
||||
│ │ "post_tag": {...}, │ │
|
||||
│ │ "product_cat": {...} │ │
|
||||
│ │ }, │ │
|
||||
│ │ "timestamp": "2025-11-22T10:15:30+00:00", │ │
|
||||
│ │ "plugin_connection_enabled": true, │ │
|
||||
│ │ "two_way_sync_enabled": true │ │
|
||||
│ │ } │ │
|
||||
│ └────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Debug Logging (NEW - Post Request Logging): │
|
||||
│ ├─ Log: Request URL │
|
||||
│ ├─ Log: Request payload (sanitized) │
|
||||
│ ├─ Log: Response status code │
|
||||
│ ├─ Log: Response body (first 500 chars) │
|
||||
│ └─ Log: Success/error with integration ID │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ IGNY8 BACKEND - Store Structure (modules/integration/views.py │
|
||||
│ update_site_structure action) │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 1. Authenticate request │
|
||||
│ ├─ Check Bearer token │
|
||||
│ └─ Verify user owns this integration │
|
||||
│ │
|
||||
│ 2. Extract payload │
|
||||
│ ├─ post_types │
|
||||
│ ├─ taxonomies │
|
||||
│ ├─ timestamp (optional, defaults to now) │
|
||||
│ ├─ plugin_connection_enabled │
|
||||
│ └─ two_way_sync_enabled │
|
||||
│ │
|
||||
│ 3. Store in SiteIntegration.config_json │
|
||||
│ ┌────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ config_json = { │ │
|
||||
│ │ "content_types": { │ │
|
||||
│ │ "post_types": {...}, │ │
|
||||
│ │ "taxonomies": {...}, │ │
|
||||
│ │ "last_structure_fetch": "2025-11-22T10:15:30+00:00" │ │
|
||||
│ │ }, │ │
|
||||
│ │ "plugin_connection_enabled": true, │ │
|
||||
│ │ "two_way_sync_enabled": true, │ │
|
||||
│ │ ... other config fields ... │ │
|
||||
│ │ } │ │
|
||||
│ └────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 4. Return Success Response │
|
||||
│ ┌────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ { │ │
|
||||
│ │ "success": true, │ │
|
||||
│ │ "data": { │ │
|
||||
│ │ "message": "Site structure updated successfully", │ │
|
||||
│ │ "post_types_count": 3, │ │
|
||||
│ │ "taxonomies_count": 3, │ │
|
||||
│ │ "last_structure_fetch": "2025-11-22T10:15:30+00:00" │ │
|
||||
│ │ } │ │
|
||||
│ │ } │ │
|
||||
│ └────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 5. Database save │
|
||||
│ └─ SiteIntegration record updated │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ WORDPRESS PLUGIN - Confirm Success & Update Options │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 1. Response Received (success == true) │
|
||||
│ ├─ Show success message to user │
|
||||
│ ├─ Log: "Site structure synced successfully" │
|
||||
│ └─ Update option: igny8_last_structure_sync = timestamp │
|
||||
│ │
|
||||
│ 2. New Options Created: │
|
||||
│ ├─ igny8_structure_synced = 1 (flag for status checking) │
|
||||
│ └─ igny8_last_structure_sync = unix timestamp │
|
||||
│ │
|
||||
│ 3. User Feedback: │
|
||||
│ ├─ "Successfully connected to IGNY8 API" │
|
||||
│ ├─ "Site structure synced successfully" ← NEW MESSAGE │
|
||||
│ └─ Or: "Connected but structure sync will be retried" (non-blocking) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ IGNY8 FRONTEND - Fetch & Display Content Types │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 1. User navigates to Site Settings → Content Types Tab │
|
||||
│ │
|
||||
│ 2. Frontend queries backend: │
|
||||
│ GET /v1/integration/integrations/{integration_id}/content-types/ │
|
||||
│ │
|
||||
│ 3. Backend processes request (content_types_summary action): │
|
||||
│ ├─ Get stored content_types from config_json │
|
||||
│ ├─ Count synced items in Content model │
|
||||
│ ├─ Count synced items in ContentTaxonomy model │
|
||||
│ ├─ Compute synced_count for each post type │
|
||||
│ └─ Compute synced_count for each taxonomy │
|
||||
│ │
|
||||
│ 4. Backend Response: │
|
||||
│ ┌────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ { │ │
|
||||
│ │ "success": true, │ │
|
||||
│ │ "data": { │ │
|
||||
│ │ "post_types": { │ │
|
||||
│ │ "post": { │ │
|
||||
│ │ "label": "Posts", │ │
|
||||
│ │ "count": 50, ← Total in WordPress │ │
|
||||
│ │ "synced_count": 30, ← Synced to IGNY8 │ │
|
||||
│ │ "enabled": true, │ │
|
||||
│ │ "fetch_limit": 100 │ │
|
||||
│ │ }, │ │
|
||||
│ │ "page": {...}, │ │
|
||||
│ │ "product": {...} │ │
|
||||
│ │ }, │ │
|
||||
│ │ "taxonomies": { │ │
|
||||
│ │ "category": { │ │
|
||||
│ │ "label": "Categories", │ │
|
||||
│ │ "count": 12, ← Total in WordPress │ │
|
||||
│ │ "synced_count": 12, ← Synced to IGNY8 │ │
|
||||
│ │ "enabled": true, │ │
|
||||
│ │ "fetch_limit": 100 │ │
|
||||
│ │ }, │ │
|
||||
│ │ "post_tag": {...}, │ │
|
||||
│ │ "product_cat": {...} │ │
|
||||
│ │ }, │ │
|
||||
│ │ "last_structure_fetch": "2025-11-22T10:15:30+00:00", │ │
|
||||
│ │ "plugin_connection_enabled": true, │ │
|
||||
│ │ "two_way_sync_enabled": true │ │
|
||||
│ │ } │ │
|
||||
│ │ } │ │
|
||||
│ └────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 5. Frontend Renders: │
|
||||
│ ┌────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Content Types │ │
|
||||
│ │ ┌──────────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ Post Types │ │ │
|
||||
│ │ │ ┌────────────────────────────────────────────────────┐ │ │ │
|
||||
│ │ │ │ Posts 50 total · 30 synced │ │ │ │
|
||||
│ │ │ │ Enabled Limit: 100 │ │ │ │
|
||||
│ │ │ └────────────────────────────────────────────────────┘ │ │ │
|
||||
│ │ │ ┌────────────────────────────────────────────────────┐ │ │ │
|
||||
│ │ │ │ Pages 10 total · 8 synced │ │ │ │
|
||||
│ │ │ │ Enabled Limit: 100 │ │ │ │
|
||||
│ │ │ └────────────────────────────────────────────────────┘ │ │ │
|
||||
│ │ │ ┌────────────────────────────────────────────────────┐ │ │ │
|
||||
│ │ │ │ Products 100 total · 45 synced │ │ │ │
|
||||
│ │ │ │ Enabled Limit: 100 │ │ │ │
|
||||
│ │ │ └────────────────────────────────────────────────────┘ │ │ │
|
||||
│ │ └──────────────────────────────────────────────────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ ┌──────────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ Taxonomies │ │ │
|
||||
│ │ │ ┌────────────────────────────────────────────────────┐ │ │ │
|
||||
│ │ │ │ Categories 12 total · 12 synced │ │ │ │
|
||||
│ │ │ │ Enabled Limit: 100 │ │ │ │
|
||||
│ │ │ └────────────────────────────────────────────────────┘ │ │ │
|
||||
│ │ │ ┌────────────────────────────────────────────────────┐ │ │ │
|
||||
│ │ │ │ Tags 89 total · 60 synced │ │ │ │
|
||||
│ │ │ │ Enabled Limit: 100 │ │ │ │
|
||||
│ │ │ └────────────────────────────────────────────────────┘ │ │ │
|
||||
│ │ └──────────────────────────────────────────────────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ Structure last fetched: 2025-11-22 10:15:30 UTC │ │
|
||||
│ └────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Daily Cron Job - Automatic Updates
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ WordPress Cron - Daily Schedule (igny8_sync_site_structure) │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Every 24 hours: │
|
||||
│ ├─ Trigger: do_action('igny8_sync_site_structure') │
|
||||
│ ├─ Call: igny8_sync_site_structure_to_backend() │
|
||||
│ ├─ Same flow as above (Get structure → Push to backend) │
|
||||
│ ├─ Updates counts and structure if changed │
|
||||
│ └─ Ensures frontend always has current data │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling & Logging Flow
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────────┐
|
||||
│ Error Detection & Logging │
|
||||
├──────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ If query fails: │
|
||||
│ ├─ Log: "Failed to fetch integrations. Error: [details]" │
|
||||
│ └─ Return: false (non-blocking) │
|
||||
│ │
|
||||
│ If integration not found: │
|
||||
│ ├─ Log: "Could not find valid WordPress integration for site {id}" │
|
||||
│ ├─ Log: "Response data: [full response]" │
|
||||
│ └─ Return: false (non-blocking) │
|
||||
│ │
|
||||
│ If POST fails: │
|
||||
│ ├─ Log: "Failed to sync site structure to integration {id}" │
|
||||
│ ├─ Log: "Error: [error message]" │
|
||||
│ ├─ Log: "Full response: [response JSON]" │
|
||||
│ └─ Return: false (non-blocking) │
|
||||
│ │
|
||||
│ If successful: │
|
||||
│ ├─ Log: "Site structure synced successfully to integration {id}" │
|
||||
│ ├─ Update: igny8_structure_synced option │
|
||||
│ ├─ Update: igny8_last_structure_sync timestamp │
|
||||
│ └─ Return: true │
|
||||
│ │
|
||||
│ All logs go to: wp-content/debug.log │
|
||||
│ To enable: define('WP_DEBUG_LOG', true) in wp-config.php │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
✅ **Reliable bidirectional data flow**
|
||||
- WordPress → Backend: Structure pushed on connection and daily
|
||||
- Backend → Frontend: Structure retrieved and displayed with sync counts
|
||||
- All steps logged and error-handled
|
||||
- Non-blocking approach ensures connection always succeeds
|
||||
|
||||
✅ **User visibility**
|
||||
- Clear success/failure messages
|
||||
- Debug logs provide troubleshooting info
|
||||
- Frontend shows current status and counts
|
||||
|
||||
✅ **Maintenance**
|
||||
- Automatic daily updates keep data fresh
|
||||
- Error handling prevents sync failures from breaking the system
|
||||
- Complete audit trail in logs
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,180 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: IGNY8 WordPress Bridge
|
||||
* Plugin URI: https://github.com/your-repo/igny8-ai-os
|
||||
* Description: Lightweight bridge plugin that connects WordPress to IGNY8 API for one-way content publishing.
|
||||
* Version: 1.1.0
|
||||
* Author: Your Name
|
||||
* Author URI: https://yourwebsite.com
|
||||
* License: GPL v2 or later
|
||||
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||
* Text Domain: igny8-bridge
|
||||
* Domain Path: /languages
|
||||
* Requires at least: 5.0
|
||||
* Requires PHP: 7.4
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Define plugin constants
|
||||
define('IGNY8_BRIDGE_VERSION', '1.1.0');
|
||||
define('IGNY8_BRIDGE_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
||||
define('IGNY8_BRIDGE_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||
define('IGNY8_BRIDGE_PLUGIN_FILE', __FILE__);
|
||||
define('IGNY8_BRIDGE_PLUGIN_BASENAME', plugin_basename(__FILE__));
|
||||
|
||||
/**
|
||||
* Main plugin class
|
||||
*/
|
||||
class Igny8Bridge {
|
||||
|
||||
/**
|
||||
* Single instance of the class
|
||||
*
|
||||
* @var Igny8Bridge
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Get single instance
|
||||
*
|
||||
* @return Igny8Bridge
|
||||
*/
|
||||
public static function get_instance() {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize plugin
|
||||
*/
|
||||
private function init() {
|
||||
// Load core files
|
||||
$this->load_dependencies();
|
||||
|
||||
// Initialize hooks
|
||||
add_action('plugins_loaded', array($this, 'load_plugin_textdomain'));
|
||||
add_action('init', array($this, 'init_plugin'));
|
||||
|
||||
// Activation/Deactivation hooks
|
||||
register_activation_hook(__FILE__, array($this, 'activate'));
|
||||
register_deactivation_hook(__FILE__, array($this, 'deactivate'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Load plugin dependencies
|
||||
*/
|
||||
private function load_dependencies() {
|
||||
// Core classes
|
||||
require_once IGNY8_BRIDGE_PLUGIN_DIR . 'includes/functions.php';
|
||||
require_once IGNY8_BRIDGE_PLUGIN_DIR . 'includes/class-igny8-logger.php'; // Load logger first
|
||||
require_once IGNY8_BRIDGE_PLUGIN_DIR . 'includes/class-igny8-api.php';
|
||||
require_once IGNY8_BRIDGE_PLUGIN_DIR . 'includes/class-igny8-site.php';
|
||||
require_once IGNY8_BRIDGE_PLUGIN_DIR . 'includes/class-igny8-rest-api.php';
|
||||
// Webhooks removed - using API key authentication only
|
||||
|
||||
// Webhook logs (used in admin and frontend)
|
||||
require_once IGNY8_BRIDGE_PLUGIN_DIR . 'includes/class-igny8-webhook-logs.php';
|
||||
|
||||
// Template functions and loader (for frontend content display)
|
||||
require_once IGNY8_BRIDGE_PLUGIN_DIR . 'includes/template-functions.php';
|
||||
require_once IGNY8_BRIDGE_PLUGIN_DIR . 'includes/class-igny8-template-loader.php';
|
||||
|
||||
// Admin classes (only in admin)
|
||||
if (is_admin()) {
|
||||
require_once IGNY8_BRIDGE_PLUGIN_DIR . 'admin/class-admin.php';
|
||||
require_once IGNY8_BRIDGE_PLUGIN_DIR . 'admin/class-admin-columns.php';
|
||||
require_once IGNY8_BRIDGE_PLUGIN_DIR . 'admin/class-post-meta-boxes.php';
|
||||
}
|
||||
|
||||
// IGNY8 to WordPress publishing (one-way only)
|
||||
require_once IGNY8_BRIDGE_PLUGIN_DIR . 'sync/igny8-to-wp.php';
|
||||
|
||||
// Data collection
|
||||
require_once IGNY8_BRIDGE_PLUGIN_DIR . 'data/site-collection.php';
|
||||
require_once IGNY8_BRIDGE_PLUGIN_DIR . 'data/semantic-mapping.php';
|
||||
require_once IGNY8_BRIDGE_PLUGIN_DIR . 'data/link-graph.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Load plugin textdomain
|
||||
*/
|
||||
public function load_plugin_textdomain() {
|
||||
load_plugin_textdomain(
|
||||
'igny8-bridge',
|
||||
false,
|
||||
dirname(IGNY8_BRIDGE_PLUGIN_BASENAME) . '/languages'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize plugin functionality
|
||||
*/
|
||||
public function init_plugin() {
|
||||
// Register post meta fields
|
||||
igny8_register_post_meta();
|
||||
|
||||
// Register taxonomies
|
||||
igny8_register_taxonomies();
|
||||
|
||||
// Initialize admin (if in admin)
|
||||
if (is_admin()) {
|
||||
Igny8Admin::get_instance();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin activation
|
||||
*/
|
||||
public function activate() {
|
||||
// Register post meta and taxonomies
|
||||
igny8_register_post_meta();
|
||||
igny8_register_taxonomies();
|
||||
|
||||
// Flush rewrite rules
|
||||
flush_rewrite_rules();
|
||||
|
||||
// Set default options
|
||||
if (!get_option('igny8_bridge_version')) {
|
||||
add_option('igny8_bridge_version', IGNY8_BRIDGE_VERSION);
|
||||
}
|
||||
|
||||
// Set default post status option
|
||||
if (!get_option('igny8_default_post_status')) {
|
||||
add_option('igny8_default_post_status', 'draft');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin deactivation
|
||||
*/
|
||||
public function deactivate() {
|
||||
// Flush rewrite rules
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize plugin
|
||||
*/
|
||||
function igny8_bridge_init() {
|
||||
return Igny8Bridge::get_instance();
|
||||
}
|
||||
|
||||
// Start the plugin
|
||||
igny8_bridge_init();
|
||||
|
||||
@@ -1,495 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* IGNY8 API Client Class
|
||||
*
|
||||
* Handles all communication with IGNY8 API v1.0
|
||||
* Follows WORDPRESS-PLUGIN-INTEGRATION.md guidelines
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Igny8API Class
|
||||
*/
|
||||
class Igny8API {
|
||||
|
||||
/**
|
||||
* API base URL
|
||||
* Note: Base is /api, endpoints should include /v1/ prefix
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $base_url = 'https://api.igny8.com/api';
|
||||
|
||||
/**
|
||||
* API key (used as access token)
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
private $access_token = null;
|
||||
|
||||
/**
|
||||
* Whether authentication is via API key (always true now)
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $api_key_auth = true;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* Only uses API key for authentication
|
||||
*/
|
||||
public function __construct() {
|
||||
if (function_exists('igny8_get_secure_option')) {
|
||||
$api_key = igny8_get_secure_option('igny8_api_key');
|
||||
} else {
|
||||
$api_key = get_option('igny8_api_key');
|
||||
}
|
||||
|
||||
// API key is the only authentication method
|
||||
if (!empty($api_key)) {
|
||||
$this->access_token = $api_key;
|
||||
$this->api_key_auth = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect using API key
|
||||
* Tests connection by calling /v1/integration/integrations/test-connection/ endpoint
|
||||
*
|
||||
* @param string $api_key API key from IGNY8 app
|
||||
* @param int $site_id Site ID from IGNY8 app
|
||||
* @return bool True on success, false on failure
|
||||
*/
|
||||
public function connect($api_key, $site_id = null) {
|
||||
if (empty($api_key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Store API key
|
||||
if (function_exists('igny8_store_secure_option')) {
|
||||
igny8_store_secure_option('igny8_api_key', $api_key);
|
||||
} else {
|
||||
update_option('igny8_api_key', $api_key);
|
||||
}
|
||||
|
||||
$this->access_token = $api_key;
|
||||
$this->api_key_auth = true;
|
||||
|
||||
// If site_id provided, test connection to integration endpoint
|
||||
if (!empty($site_id)) {
|
||||
$test_response = $this->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']) {
|
||||
$timestamp = current_time('timestamp');
|
||||
update_option('igny8_last_api_health_check', $timestamp);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fallback: if no site_id, just verify API key exists by making a simple call
|
||||
// This tests that the API key is valid format at least
|
||||
$timestamp = current_time('timestamp');
|
||||
update_option('igny8_last_api_health_check', $timestamp);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if API is authenticated
|
||||
*
|
||||
* @return bool True if authenticated, false otherwise
|
||||
*/
|
||||
public function is_authenticated() {
|
||||
return !empty($this->access_token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the API base URL
|
||||
*
|
||||
* @return string API base URL
|
||||
*/
|
||||
public function get_api_base() {
|
||||
return $this->base_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse unified API response
|
||||
*
|
||||
* @param array|WP_Error $response HTTP response
|
||||
* @return array Parsed response
|
||||
*/
|
||||
private function parse_response($response) {
|
||||
if (is_wp_error($response)) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'error' => $response->get_error_message(),
|
||||
'http_status' => 0
|
||||
);
|
||||
}
|
||||
|
||||
$status_code = wp_remote_retrieve_response_code($response);
|
||||
$raw_body = wp_remote_retrieve_body($response);
|
||||
$body = json_decode($raw_body, true);
|
||||
|
||||
// Handle non-JSON responses — allow empty arrays/objects but detect JSON decode errors
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'error' => 'Invalid JSON response: ' . json_last_error_msg(),
|
||||
'raw_body' => substr($raw_body, 0, 200),
|
||||
'http_status' => $status_code
|
||||
);
|
||||
}
|
||||
|
||||
// Check if response follows unified format
|
||||
if (isset($body['success'])) {
|
||||
$body['http_status'] = $status_code;
|
||||
|
||||
// Handle throttling errors (429) - extract retry delay from error message
|
||||
if ($status_code === 429 && isset($body['error'])) {
|
||||
// Extract delay from error message like "Request was throttled. Expected available in 1 second."
|
||||
if (preg_match('/Expected available in (\d+) second/i', $body['error'], $matches)) {
|
||||
$body['retry_after'] = intval($matches[1]);
|
||||
} elseif (preg_match('/(\d+) second/i', $body['error'], $matches)) {
|
||||
$body['retry_after'] = intval($matches[1]);
|
||||
} else {
|
||||
// Default to 2 seconds if we can't parse it
|
||||
$body['retry_after'] = 2;
|
||||
}
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
// Legacy format - wrap in unified format
|
||||
if ($status_code >= 200 && $status_code < 300) {
|
||||
return array(
|
||||
'success' => true,
|
||||
'data' => $body,
|
||||
'http_status' => $status_code
|
||||
);
|
||||
} else {
|
||||
$error_message = $body['detail'] ?? 'HTTP ' . $status_code . ' error';
|
||||
|
||||
// Handle throttling in legacy format
|
||||
$retry_after = null;
|
||||
if ($status_code === 429) {
|
||||
if (preg_match('/Expected available in (\d+) second/i', $error_message, $matches)) {
|
||||
$retry_after = intval($matches[1]);
|
||||
} elseif (preg_match('/(\d+) second/i', $error_message, $matches)) {
|
||||
$retry_after = intval($matches[1]);
|
||||
} else {
|
||||
$retry_after = 2;
|
||||
}
|
||||
}
|
||||
|
||||
$result = array(
|
||||
'success' => false,
|
||||
'error' => $error_message,
|
||||
'http_status' => $status_code,
|
||||
'raw_error' => $body
|
||||
);
|
||||
|
||||
if ($retry_after !== null) {
|
||||
$result['retry_after'] = $retry_after;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get headers with authentication
|
||||
* Uses Bearer token format for API key authentication
|
||||
*
|
||||
* @return array Headers array
|
||||
*/
|
||||
private function get_headers() {
|
||||
$headers = array(
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json'
|
||||
);
|
||||
|
||||
if (!empty($this->access_token)) {
|
||||
$headers['Authorization'] = 'Bearer ' . $this->access_token;
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make GET request with automatic retry on throttling
|
||||
*
|
||||
* @param string $endpoint API endpoint (e.g. /v1/auth/sites/ or /v1/integration/integrations/)
|
||||
* @param int $max_retries Maximum number of retries for throttled requests (default: 3)
|
||||
* @return array Response data
|
||||
*/
|
||||
public function get($endpoint, $max_retries = 3) {
|
||||
if (!$this->is_authenticated()) {
|
||||
return array('success' => false, 'error' => 'Not authenticated', 'http_status' => 401);
|
||||
}
|
||||
// Ensure endpoint starts with /v1
|
||||
if (strpos($endpoint, '/v1/') === false) {
|
||||
if (strpos($endpoint, '/') !== 0) {
|
||||
$endpoint = '/' . $endpoint;
|
||||
}
|
||||
if (strpos($endpoint, '/v1') !== 0) {
|
||||
$endpoint = '/v1' . $endpoint;
|
||||
}
|
||||
}
|
||||
|
||||
$url = $this->base_url . $endpoint;
|
||||
$headers = $this->get_headers();
|
||||
$retry_count = 0;
|
||||
|
||||
while ($retry_count <= $max_retries) {
|
||||
// Debug logging (enable with WP_DEBUG or IGNY8_DEBUG constant)
|
||||
$debug_enabled = (defined('WP_DEBUG') && WP_DEBUG) || (defined('IGNY8_DEBUG') && IGNY8_DEBUG);
|
||||
if ($debug_enabled) {
|
||||
error_log(sprintf(
|
||||
'IGNY8 DEBUG GET: %s | Headers: %s',
|
||||
$url,
|
||||
json_encode(array_merge($headers, array('Authorization' => 'Bearer ***')))
|
||||
));
|
||||
}
|
||||
|
||||
$response = wp_remote_get($url, array(
|
||||
'headers' => $headers,
|
||||
'timeout' => 30
|
||||
));
|
||||
|
||||
// Debug response
|
||||
if ($debug_enabled) {
|
||||
$status_code = wp_remote_retrieve_response_code($response);
|
||||
$response_body = wp_remote_retrieve_body($response);
|
||||
error_log(sprintf(
|
||||
'IGNY8 DEBUG RESPONSE: Status=%s | Body=%s',
|
||||
$status_code,
|
||||
substr($response_body, 0, 500)
|
||||
));
|
||||
}
|
||||
|
||||
$body = $this->parse_response($response);
|
||||
|
||||
// If throttled (429), retry after the specified delay
|
||||
if (isset($body['http_status']) && $body['http_status'] === 429 && $retry_count < $max_retries) {
|
||||
$retry_after = isset($body['retry_after']) ? $body['retry_after'] : 2;
|
||||
|
||||
// Add a small buffer (0.5 seconds) to ensure we wait long enough
|
||||
$wait_time = $retry_after + 0.5;
|
||||
$wait_seconds = (int) ceil($wait_time); // Convert to integer, rounding up
|
||||
|
||||
// Log retry attempt
|
||||
if ($debug_enabled) {
|
||||
error_log(sprintf(
|
||||
'IGNY8 DEBUG: Request throttled, retrying after %.1f seconds (attempt %d/%d)',
|
||||
$wait_time,
|
||||
$retry_count + 1,
|
||||
$max_retries
|
||||
));
|
||||
}
|
||||
|
||||
// Wait before retrying
|
||||
sleep($wait_seconds);
|
||||
$retry_count++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Not throttled or max retries reached, return response
|
||||
// API keys don't expire, so no refresh logic needed
|
||||
// If 401, the API key is invalid or revoked
|
||||
return $body;
|
||||
}
|
||||
|
||||
// Should never reach here, but return last response if we do
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make POST request with automatic retry on throttling
|
||||
*
|
||||
* @param string $endpoint API endpoint (e.g. /v1/integration/integrations/)
|
||||
* @param array $data Request data
|
||||
* @param int $max_retries Maximum number of retries for throttled requests (default: 3)
|
||||
* @return array Response data
|
||||
*/
|
||||
public function post($endpoint, $data, $max_retries = 3) {
|
||||
// Special case: test-connection endpoint allows API key in request body
|
||||
// So we don't require pre-authentication for this endpoint
|
||||
$is_test_connection = (strpos($endpoint, 'test-connection') !== false);
|
||||
$has_api_key_in_data = !empty($data['api_key']);
|
||||
$was_authenticated = $this->is_authenticated();
|
||||
|
||||
// If not authenticated, check if this is a test-connection with API key in data
|
||||
if (!$was_authenticated) {
|
||||
if ($is_test_connection && $has_api_key_in_data) {
|
||||
// Temporarily set the API key for this request
|
||||
$temp_api_key = $this->access_token;
|
||||
$this->access_token = $data['api_key'];
|
||||
} else {
|
||||
return array('success' => false, 'error' => 'Not authenticated', 'http_status' => 401);
|
||||
}
|
||||
}
|
||||
// Ensure endpoint starts with /v1
|
||||
if (strpos($endpoint, '/v1/') === false) {
|
||||
if (strpos($endpoint, '/') !== 0) {
|
||||
$endpoint = '/' . $endpoint;
|
||||
}
|
||||
if (strpos($endpoint, '/v1') !== 0) {
|
||||
$endpoint = '/v1' . $endpoint;
|
||||
}
|
||||
}
|
||||
|
||||
$retry_count = 0;
|
||||
|
||||
while ($retry_count <= $max_retries) {
|
||||
$response = wp_remote_post($this->base_url . $endpoint, array(
|
||||
'headers' => $this->get_headers(),
|
||||
'body' => json_encode($data),
|
||||
'timeout' => 60
|
||||
));
|
||||
|
||||
$body = $this->parse_response($response);
|
||||
|
||||
// If throttled (429), retry after the specified delay
|
||||
if (isset($body['http_status']) && $body['http_status'] === 429 && $retry_count < $max_retries) {
|
||||
$retry_after = isset($body['retry_after']) ? $body['retry_after'] : 2;
|
||||
|
||||
// Add a small buffer (0.5 seconds) to ensure we wait long enough
|
||||
$wait_time = $retry_after + 0.5;
|
||||
$wait_seconds = (int) ceil($wait_time); // Convert to integer, rounding up
|
||||
|
||||
// Log retry attempt
|
||||
$debug_enabled = (defined('WP_DEBUG') && WP_DEBUG) || (defined('IGNY8_DEBUG') && IGNY8_DEBUG);
|
||||
if ($debug_enabled) {
|
||||
error_log(sprintf(
|
||||
'IGNY8 DEBUG: Request throttled, retrying after %.1f seconds (attempt %d/%d)',
|
||||
$wait_time,
|
||||
$retry_count + 1,
|
||||
$max_retries
|
||||
));
|
||||
}
|
||||
|
||||
// Wait before retrying
|
||||
sleep($wait_seconds);
|
||||
$retry_count++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Not throttled or max retries reached, return response
|
||||
// Restore original access token if we temporarily set it
|
||||
if ($is_test_connection && $has_api_key_in_data && !$was_authenticated) {
|
||||
$this->access_token = isset($temp_api_key) ? $temp_api_key : null;
|
||||
}
|
||||
return $body;
|
||||
}
|
||||
|
||||
// Should never reach here, but return last response if we do
|
||||
// Restore original access token if we temporarily set it
|
||||
if ($is_test_connection && $has_api_key_in_data && !$was_authenticated) {
|
||||
$this->access_token = isset($temp_api_key) ? $temp_api_key : null;
|
||||
}
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make PUT request with automatic retry on throttling
|
||||
*
|
||||
* @param string $endpoint API endpoint (e.g. /v1/integration/integrations/1/update-structure/)
|
||||
* @param array $data Request data
|
||||
* @param int $max_retries Maximum number of retries for throttled requests (default: 3)
|
||||
* @return array Response data
|
||||
*/
|
||||
public function put($endpoint, $data, $max_retries = 3) {
|
||||
if (!$this->is_authenticated()) {
|
||||
return array('success' => false, 'error' => 'Not authenticated', 'http_status' => 401);
|
||||
}
|
||||
// Ensure endpoint starts with /v1
|
||||
if (strpos($endpoint, '/v1/') === false) {
|
||||
if (strpos($endpoint, '/') !== 0) {
|
||||
$endpoint = '/' . $endpoint;
|
||||
}
|
||||
if (strpos($endpoint, '/v1') !== 0) {
|
||||
$endpoint = '/v1' . $endpoint;
|
||||
}
|
||||
}
|
||||
|
||||
$retry_count = 0;
|
||||
|
||||
while ($retry_count <= $max_retries) {
|
||||
$response = wp_remote_request($this->base_url . $endpoint, array(
|
||||
'method' => 'PUT',
|
||||
'headers' => $this->get_headers(),
|
||||
'body' => json_encode($data),
|
||||
'timeout' => 60
|
||||
));
|
||||
|
||||
$body = $this->parse_response($response);
|
||||
|
||||
// If throttled (429), retry after the specified delay
|
||||
if (isset($body['http_status']) && $body['http_status'] === 429 && $retry_count < $max_retries) {
|
||||
$retry_after = isset($body['retry_after']) ? $body['retry_after'] : 2;
|
||||
$wait_time = $retry_after + 0.5;
|
||||
$wait_seconds = (int) ceil($wait_time); // Convert to integer, rounding up
|
||||
|
||||
$debug_enabled = (defined('WP_DEBUG') && WP_DEBUG) || (defined('IGNY8_DEBUG') && IGNY8_DEBUG);
|
||||
if ($debug_enabled) {
|
||||
error_log(sprintf(
|
||||
'IGNY8 DEBUG: Request throttled, retrying after %.1f seconds (attempt %d/%d)',
|
||||
$wait_time,
|
||||
$retry_count + 1,
|
||||
$max_retries
|
||||
));
|
||||
}
|
||||
|
||||
sleep($wait_seconds);
|
||||
$retry_count++;
|
||||
continue;
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make DELETE request
|
||||
*
|
||||
* @param string $endpoint API endpoint
|
||||
* @return array Response data
|
||||
*/
|
||||
public function delete($endpoint) {
|
||||
if (!$this->is_authenticated()) {
|
||||
return array('success' => false, 'error' => 'Not authenticated', 'http_status' => 401);
|
||||
}
|
||||
$response = wp_remote_request($this->base_url . $endpoint, array(
|
||||
'method' => 'DELETE',
|
||||
'headers' => $this->get_headers(),
|
||||
'timeout' => 30
|
||||
));
|
||||
|
||||
return $this->parse_response($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get access token
|
||||
*
|
||||
* @return string|null Access token
|
||||
*/
|
||||
public function get_access_token() {
|
||||
return $this->access_token;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Link Insertion Queue
|
||||
*
|
||||
* Queues and processes link recommendations from IGNY8 Linker
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue link insertion
|
||||
*
|
||||
* @param array $link_data Link data
|
||||
* @return int|false Queue ID or false on failure
|
||||
*/
|
||||
function igny8_queue_link_insertion($link_data) {
|
||||
if (!igny8_is_connection_enabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$queue = get_option('igny8_link_queue', array());
|
||||
|
||||
$queue_item = array(
|
||||
'id' => uniqid('link_', true),
|
||||
'post_id' => intval($link_data['post_id']),
|
||||
'target_url' => esc_url_raw($link_data['target_url']),
|
||||
'anchor' => sanitize_text_field($link_data['anchor']),
|
||||
'source' => sanitize_text_field($link_data['source'] ?? 'igny8_linker'),
|
||||
'priority' => sanitize_text_field($link_data['priority'] ?? 'normal'),
|
||||
'status' => 'pending',
|
||||
'created_at' => $link_data['created_at'] ?? current_time('mysql'),
|
||||
'attempts' => 0
|
||||
);
|
||||
|
||||
$queue[] = $queue_item;
|
||||
|
||||
// Limit queue size (keep last 1000 items)
|
||||
if (count($queue) > 1000) {
|
||||
$queue = array_slice($queue, -1000);
|
||||
}
|
||||
|
||||
update_option('igny8_link_queue', $queue);
|
||||
|
||||
// Trigger processing if not already scheduled
|
||||
if (!wp_next_scheduled('igny8_process_link_queue')) {
|
||||
wp_schedule_single_event(time() + 60, 'igny8_process_link_queue');
|
||||
}
|
||||
|
||||
return $queue_item['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Process link insertion queue
|
||||
*/
|
||||
function igny8_process_link_queue() {
|
||||
if (!igny8_is_connection_enabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (function_exists('igny8_is_module_enabled') && !igny8_is_module_enabled('linker')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$queue = get_option('igny8_link_queue', array());
|
||||
|
||||
if (empty($queue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Process up to 10 items per run
|
||||
$processed = 0;
|
||||
$max_per_run = 10;
|
||||
|
||||
foreach ($queue as $key => $item) {
|
||||
if ($processed >= $max_per_run) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ($item['status'] !== 'pending') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = igny8_insert_link_into_post($item);
|
||||
|
||||
if ($result['success']) {
|
||||
$queue[$key]['status'] = 'completed';
|
||||
$queue[$key]['completed_at'] = current_time('mysql');
|
||||
} else {
|
||||
$queue[$key]['attempts']++;
|
||||
|
||||
if ($queue[$key]['attempts'] >= 3) {
|
||||
$queue[$key]['status'] = 'failed';
|
||||
$queue[$key]['error'] = $result['error'] ?? 'Unknown error';
|
||||
}
|
||||
}
|
||||
|
||||
$processed++;
|
||||
}
|
||||
|
||||
update_option('igny8_link_queue', $queue);
|
||||
|
||||
// Schedule next run if there are pending items
|
||||
$has_pending = false;
|
||||
foreach ($queue as $item) {
|
||||
if ($item['status'] === 'pending') {
|
||||
$has_pending = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($has_pending && !wp_next_scheduled('igny8_process_link_queue')) {
|
||||
wp_schedule_single_event(time() + 60, 'igny8_process_link_queue');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert link into post content
|
||||
*
|
||||
* @param array $link_item Link queue item
|
||||
* @return array Result
|
||||
*/
|
||||
function igny8_insert_link_into_post($link_item) {
|
||||
$post_id = $link_item['post_id'];
|
||||
$target_url = $link_item['target_url'];
|
||||
$anchor = $link_item['anchor'];
|
||||
|
||||
$post = get_post($post_id);
|
||||
|
||||
if (!$post) {
|
||||
return array('success' => false, 'error' => 'Post not found');
|
||||
}
|
||||
|
||||
$content = $post->post_content;
|
||||
|
||||
// Check if link already exists
|
||||
if (strpos($content, $target_url) !== false) {
|
||||
return array('success' => true, 'message' => 'Link already exists');
|
||||
}
|
||||
|
||||
// Find first occurrence of anchor text not already in a link
|
||||
$anchor_escaped = preg_quote($anchor, '/');
|
||||
|
||||
// Pattern to find anchor text that's not inside an <a> tag
|
||||
// This is a simplified approach - find anchor text and check if it's not in a link
|
||||
$pattern = '/\b' . $anchor_escaped . '\b/i';
|
||||
|
||||
if (preg_match_all($pattern, $content, $matches, PREG_OFFSET_CAPTURE)) {
|
||||
foreach ($matches[0] as $match) {
|
||||
$position = $match[1];
|
||||
$length = strlen($match[0]);
|
||||
|
||||
// Check if this position is inside an <a> tag
|
||||
$before = substr($content, 0, $position);
|
||||
$after = substr($content, $position + $length);
|
||||
|
||||
// Count unclosed <a> tags before this position
|
||||
$open_tags = substr_count($before, '<a');
|
||||
$close_tags = substr_count($before, '</a>');
|
||||
|
||||
// If not inside a link, replace it
|
||||
if ($open_tags <= $close_tags) {
|
||||
$link_html = '<a href="' . esc_url($target_url) . '">' . esc_html($anchor) . '</a>';
|
||||
$new_content = substr_replace($content, $link_html, $position, $length);
|
||||
|
||||
$result = wp_update_post(array(
|
||||
'ID' => $post_id,
|
||||
'post_content' => $new_content
|
||||
));
|
||||
|
||||
if ($result && !is_wp_error($result)) {
|
||||
return array('success' => true, 'message' => 'Link inserted');
|
||||
} else {
|
||||
return array('success' => false, 'error' => 'Failed to update post');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If anchor not found, append link at end of content
|
||||
$link_html = "\n\n<p><a href=\"" . esc_url($target_url) . "\">" . esc_html($anchor) . "</a></p>";
|
||||
$new_content = $content . $link_html;
|
||||
|
||||
$result = wp_update_post(array(
|
||||
'ID' => $post_id,
|
||||
'post_content' => $new_content
|
||||
));
|
||||
|
||||
if ($result && !is_wp_error($result)) {
|
||||
return array('success' => true, 'message' => 'Link appended');
|
||||
} else {
|
||||
return array('success' => false, 'error' => 'Failed to update post');
|
||||
}
|
||||
}
|
||||
|
||||
// Register cron hook
|
||||
add_action('igny8_process_link_queue', 'igny8_process_link_queue');
|
||||
|
||||
@@ -1,221 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* IGNY8 File Logger
|
||||
*
|
||||
* Provides file-based logging for all publish/sync workflows
|
||||
*
|
||||
* @package IGNY8_Bridge
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Igny8_Logger {
|
||||
|
||||
/**
|
||||
* Log directory path
|
||||
*/
|
||||
private static $log_dir = null;
|
||||
|
||||
/**
|
||||
* Initialize logger
|
||||
*/
|
||||
public static function init() {
|
||||
// Set log directory to plugin root/logs/publish-sync-logs
|
||||
self::$log_dir = dirname(dirname(__FILE__)) . '/logs/publish-sync-logs';
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
if (!file_exists(self::$log_dir)) {
|
||||
wp_mkdir_p(self::$log_dir);
|
||||
}
|
||||
|
||||
// Ensure directory is writable
|
||||
if (!is_writable(self::$log_dir)) {
|
||||
@chmod(self::$log_dir, 0755);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write log message to file
|
||||
*
|
||||
* @param string $message Log message
|
||||
* @param string $level Log level (INFO, WARNING, ERROR)
|
||||
* @param string $log_file Log file name (without .log extension)
|
||||
*/
|
||||
public static function log($message, $level = 'INFO', $log_file = 'publish-sync') {
|
||||
if (self::$log_dir === null) {
|
||||
self::init();
|
||||
}
|
||||
|
||||
$timestamp = current_time('Y-m-d H:i:s');
|
||||
$formatted_message = "[{$timestamp}] [{$level}] {$message}\n";
|
||||
|
||||
$file_path = self::$log_dir . '/' . $log_file . '.log';
|
||||
|
||||
// Append to log file
|
||||
error_log($formatted_message, 3, $file_path);
|
||||
|
||||
// Also log to WordPress debug.log if WP_DEBUG is enabled
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
error_log("[IGNY8] [{$level}] {$message}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log info message
|
||||
*/
|
||||
public static function info($message, $log_file = 'publish-sync') {
|
||||
self::log($message, 'INFO', $log_file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log warning message
|
||||
*/
|
||||
public static function warning($message, $log_file = 'publish-sync') {
|
||||
self::log($message, 'WARNING', $log_file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log error message
|
||||
*/
|
||||
public static function error($message, $log_file = 'publish-sync') {
|
||||
self::log($message, 'ERROR', $log_file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log API request
|
||||
*/
|
||||
public static function api_request($method, $endpoint, $data = null) {
|
||||
$message = "API REQUEST: {$method} {$endpoint}";
|
||||
if ($data) {
|
||||
$message .= "\n Data: " . json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
self::log($message, 'INFO', 'wordpress-api');
|
||||
}
|
||||
|
||||
/**
|
||||
* Log API response
|
||||
*/
|
||||
public static function api_response($status_code, $body) {
|
||||
$message = "API RESPONSE: HTTP {$status_code}";
|
||||
if ($body) {
|
||||
$body_str = is_string($body) ? $body : json_encode($body, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
$message .= "\n Body: " . substr($body_str, 0, 500);
|
||||
}
|
||||
self::log($message, 'INFO', 'wordpress-api');
|
||||
}
|
||||
|
||||
/**
|
||||
* Log API error
|
||||
*/
|
||||
public static function api_error($error_message) {
|
||||
self::log("API ERROR: {$error_message}", 'ERROR', 'wordpress-api');
|
||||
}
|
||||
|
||||
/**
|
||||
* Log webhook event
|
||||
*/
|
||||
public static function webhook($event_type, $data) {
|
||||
$message = "WEBHOOK EVENT: {$event_type}";
|
||||
if ($data) {
|
||||
$message .= "\n Data: " . json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
self::log($message, 'INFO', 'webhooks');
|
||||
}
|
||||
|
||||
/**
|
||||
* Log workflow separator
|
||||
*/
|
||||
public static function separator($title = '') {
|
||||
$line = str_repeat('=', 80);
|
||||
self::log($line);
|
||||
if ($title) {
|
||||
self::log($title);
|
||||
self::log($line);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get log file contents
|
||||
*
|
||||
* @param string $log_file Log file name
|
||||
* @param int $lines Number of lines to read (default 100, 0 for all)
|
||||
* @return string Log contents
|
||||
*/
|
||||
public static function get_log_contents($log_file = 'publish-sync', $lines = 100) {
|
||||
if (self::$log_dir === null) {
|
||||
self::init();
|
||||
}
|
||||
|
||||
$file_path = self::$log_dir . '/' . $log_file . '.log';
|
||||
|
||||
if (!file_exists($file_path)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($lines === 0) {
|
||||
return file_get_contents($file_path);
|
||||
}
|
||||
|
||||
// Read last N lines efficiently
|
||||
$file = new SplFileObject($file_path, 'r');
|
||||
$file->seek(PHP_INT_MAX);
|
||||
$total_lines = $file->key() + 1;
|
||||
|
||||
$start_line = max(0, $total_lines - $lines);
|
||||
$file->seek($start_line);
|
||||
|
||||
$content = '';
|
||||
while (!$file->eof()) {
|
||||
$content .= $file->fgets();
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear log file
|
||||
*/
|
||||
public static function clear_log($log_file = 'publish-sync') {
|
||||
if (self::$log_dir === null) {
|
||||
self::init();
|
||||
}
|
||||
|
||||
$file_path = self::$log_dir . '/' . $log_file . '.log';
|
||||
|
||||
if (file_exists($file_path)) {
|
||||
@unlink($file_path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all log files
|
||||
*/
|
||||
public static function get_log_files() {
|
||||
if (self::$log_dir === null) {
|
||||
self::init();
|
||||
}
|
||||
|
||||
if (!is_dir(self::$log_dir)) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$files = glob(self::$log_dir . '/*.log');
|
||||
$log_files = array();
|
||||
|
||||
foreach ($files as $file) {
|
||||
$log_files[] = array(
|
||||
'name' => basename($file, '.log'),
|
||||
'path' => $file,
|
||||
'size' => filesize($file),
|
||||
'modified' => filemtime($file),
|
||||
);
|
||||
}
|
||||
|
||||
return $log_files;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize logger
|
||||
Igny8_Logger::init();
|
||||
@@ -1,641 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* REST API Endpoints for IGNY8
|
||||
*
|
||||
* Provides endpoints for IGNY8 to query WordPress posts by content_id
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Igny8RestAPI Class
|
||||
*/
|
||||
class Igny8RestAPI {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action('rest_api_init', array($this, 'register_routes'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register REST API routes
|
||||
*/
|
||||
public function register_routes() {
|
||||
// Get post by IGNY8 content_id
|
||||
register_rest_route('igny8/v1', '/post-by-content-id/(?P<content_id>\d+)', array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array($this, 'get_post_by_content_id'),
|
||||
'permission_callback' => array($this, 'check_permission'),
|
||||
'args' => array(
|
||||
'content_id' => array(
|
||||
'required' => true,
|
||||
'type' => 'integer',
|
||||
'description' => 'IGNY8 content ID'
|
||||
)
|
||||
)
|
||||
));
|
||||
|
||||
// Get post by IGNY8 task_id
|
||||
register_rest_route('igny8/v1', '/post-by-task-id/(?P<task_id>\d+)', array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array($this, 'get_post_by_task_id'),
|
||||
'permission_callback' => array($this, 'check_permission'),
|
||||
'args' => array(
|
||||
'task_id' => array(
|
||||
'required' => true,
|
||||
'type' => 'integer',
|
||||
'description' => 'IGNY8 task ID'
|
||||
)
|
||||
)
|
||||
));
|
||||
|
||||
// Get post status by content_id or post_id
|
||||
register_rest_route('igny8/v1', '/post-status/(?P<id>\d+)', array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array($this, 'get_post_status'),
|
||||
'permission_callback' => array($this, 'check_permission'),
|
||||
'args' => array(
|
||||
'id' => array(
|
||||
'required' => true,
|
||||
'type' => 'integer',
|
||||
'description' => 'WordPress post ID or IGNY8 content ID (tries both)'
|
||||
)
|
||||
)
|
||||
));
|
||||
|
||||
// Site metadata - post types, taxonomies and counts (unified response format)
|
||||
register_rest_route('igny8/v1', '/site-metadata/', array(
|
||||
'methods' => 'GET',
|
||||
// We perform permission checks inside callback to ensure unified response format
|
||||
'callback' => array($this, 'get_site_metadata'),
|
||||
'permission_callback' => '__return_true',
|
||||
));
|
||||
|
||||
// Plugin status endpoint - returns connection status and API key info
|
||||
register_rest_route('igny8/v1', '/status', array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array($this, 'get_status'),
|
||||
'permission_callback' => '__return_true', // Public endpoint for health checks
|
||||
));
|
||||
|
||||
// Manual publish endpoint - for triggering WordPress publish from IGNY8
|
||||
// Route: /wp-json/igny8/v1/publish
|
||||
register_rest_route('igny8/v1', '/publish', array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array($this, 'publish_content_to_wordpress'),
|
||||
'permission_callback' => array($this, 'check_permission'),
|
||||
'args' => array(
|
||||
'content_id' => array(
|
||||
'required' => true,
|
||||
'type' => 'integer',
|
||||
'description' => 'IGNY8 content ID'
|
||||
),
|
||||
'task_id' => array(
|
||||
'required' => false,
|
||||
'type' => 'integer',
|
||||
'description' => 'IGNY8 task ID'
|
||||
)
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check API permission - uses API key only
|
||||
*
|
||||
* @param WP_REST_Request $request Request object
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
public function check_permission($request) {
|
||||
// Check if authenticated with IGNY8 via API key
|
||||
$api = new Igny8API();
|
||||
|
||||
// Accept explicit X-IGNY8-API-KEY header for incoming requests
|
||||
$header_api_key = $request->get_header('x-igny8-api-key');
|
||||
if ($header_api_key) {
|
||||
$stored_api_key = function_exists('igny8_get_secure_option') ? igny8_get_secure_option('igny8_api_key') : get_option('igny8_api_key');
|
||||
if ($stored_api_key && hash_equals($stored_api_key, $header_api_key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check Authorization Bearer header
|
||||
$auth_header = $request->get_header('Authorization');
|
||||
if ($auth_header) {
|
||||
$stored_api_key = function_exists('igny8_get_secure_option') ? igny8_get_secure_option('igny8_api_key') : get_option('igny8_api_key');
|
||||
if ($stored_api_key && strpos($auth_header, 'Bearer ' . $stored_api_key) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Allow if API key is configured (for internal use)
|
||||
if ($api->is_authenticated()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'rest_forbidden',
|
||||
__('IGNY8 API key not authenticated', 'igny8-bridge'),
|
||||
array('status' => 401)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get post by content_id
|
||||
*
|
||||
* @param WP_REST_Request $request Request object
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function get_post_by_content_id($request) {
|
||||
// Double-check connection is enabled
|
||||
if (!igny8_is_connection_enabled()) {
|
||||
return new WP_Error(
|
||||
'rest_forbidden',
|
||||
__('IGNY8 connection is disabled', 'igny8-bridge'),
|
||||
array('status' => 403)
|
||||
);
|
||||
}
|
||||
|
||||
$content_id = intval($request['content_id']);
|
||||
|
||||
// Find post by content_id meta
|
||||
$posts = get_posts(array(
|
||||
'meta_key' => '_igny8_content_id',
|
||||
'meta_value' => $content_id,
|
||||
'post_type' => 'any',
|
||||
'posts_per_page' => 1,
|
||||
'post_status' => 'any'
|
||||
));
|
||||
|
||||
if (empty($posts)) {
|
||||
return new WP_Error(
|
||||
'rest_not_found',
|
||||
__('Post not found for this content ID', 'igny8-bridge'),
|
||||
array('status' => 404)
|
||||
);
|
||||
}
|
||||
|
||||
$post = $posts[0];
|
||||
|
||||
return rest_ensure_response(array(
|
||||
'success' => true,
|
||||
'data' => array(
|
||||
'post_id' => $post->ID,
|
||||
'title' => $post->post_title,
|
||||
'status' => $post->post_status,
|
||||
'wordpress_status' => $post->post_status,
|
||||
'igny8_status' => igny8_map_wp_status_to_igny8($post->post_status),
|
||||
'url' => get_permalink($post->ID),
|
||||
'post_type' => $post->post_type,
|
||||
'content_id' => $content_id,
|
||||
'task_id' => get_post_meta($post->ID, '_igny8_task_id', true),
|
||||
'last_synced' => get_post_meta($post->ID, '_igny8_last_synced', true)
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get post by task_id
|
||||
*
|
||||
* @param WP_REST_Request $request Request object
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function get_post_by_task_id($request) {
|
||||
// Double-check connection is enabled
|
||||
if (!igny8_is_connection_enabled()) {
|
||||
return new WP_Error(
|
||||
'rest_forbidden',
|
||||
__('IGNY8 connection is disabled', 'igny8-bridge'),
|
||||
array('status' => 403)
|
||||
);
|
||||
}
|
||||
|
||||
$task_id = intval($request['task_id']);
|
||||
|
||||
// Find post by task_id meta
|
||||
$posts = get_posts(array(
|
||||
'meta_key' => '_igny8_task_id',
|
||||
'meta_value' => $task_id,
|
||||
'post_type' => 'any',
|
||||
'posts_per_page' => 1,
|
||||
'post_status' => 'any'
|
||||
));
|
||||
|
||||
if (empty($posts)) {
|
||||
return new WP_Error(
|
||||
'rest_not_found',
|
||||
__('Post not found for this task ID', 'igny8-bridge'),
|
||||
array('status' => 404)
|
||||
);
|
||||
}
|
||||
|
||||
$post = $posts[0];
|
||||
|
||||
return rest_ensure_response(array(
|
||||
'success' => true,
|
||||
'data' => array(
|
||||
'post_id' => $post->ID,
|
||||
'title' => $post->post_title,
|
||||
'status' => $post->post_status,
|
||||
'wordpress_status' => $post->post_status,
|
||||
'igny8_status' => igny8_map_wp_status_to_igny8($post->post_status),
|
||||
'url' => get_permalink($post->ID),
|
||||
'post_type' => $post->post_type,
|
||||
'task_id' => $task_id,
|
||||
'content_id' => get_post_meta($post->ID, '_igny8_content_id', true),
|
||||
'last_synced' => get_post_meta($post->ID, '_igny8_last_synced', true)
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get post status by post ID or content_id
|
||||
* Accepts either WordPress post_id or IGNY8 content_id
|
||||
*
|
||||
* @param WP_REST_Request $request Request object
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function get_post_status($request) {
|
||||
// Double-check connection is enabled
|
||||
if (!igny8_is_connection_enabled()) {
|
||||
return new WP_Error(
|
||||
'rest_forbidden',
|
||||
__('IGNY8 connection is disabled', 'igny8-bridge'),
|
||||
array('status' => 403)
|
||||
);
|
||||
}
|
||||
|
||||
$id = intval($request['id']);
|
||||
$post = null;
|
||||
$lookup_method = null;
|
||||
|
||||
// First try as WordPress post ID
|
||||
if (post_type_exists('post') || post_type_exists('page')) {
|
||||
$post = get_post($id);
|
||||
if ($post) {
|
||||
$lookup_method = 'wordpress_post_id';
|
||||
}
|
||||
}
|
||||
|
||||
// If not found, try as IGNY8 content_id
|
||||
if (!$post) {
|
||||
$posts = get_posts(array(
|
||||
'meta_key' => '_igny8_content_id',
|
||||
'meta_value' => $id,
|
||||
'post_type' => 'any',
|
||||
'posts_per_page' => 1,
|
||||
'post_status' => 'any'
|
||||
));
|
||||
|
||||
if (!empty($posts)) {
|
||||
$post = $posts[0];
|
||||
$lookup_method = 'igny8_content_id';
|
||||
}
|
||||
}
|
||||
|
||||
if (!$post) {
|
||||
return rest_ensure_response(array(
|
||||
'success' => false,
|
||||
'message' => 'Post not found',
|
||||
'searched_id' => $id
|
||||
));
|
||||
}
|
||||
|
||||
return rest_ensure_response(array(
|
||||
'success' => true,
|
||||
'data' => array(
|
||||
'post_id' => $post->ID,
|
||||
'post_status' => $post->post_status,
|
||||
'post_title' => $post->post_title,
|
||||
'post_type' => $post->post_type,
|
||||
'post_modified' => $post->post_modified,
|
||||
'post_url' => get_permalink($post->ID),
|
||||
'wordpress_status' => $post->post_status,
|
||||
'igny8_status' => igny8_map_wp_status_to_igny8($post->post_status),
|
||||
'content_id' => get_post_meta($post->ID, '_igny8_content_id', true),
|
||||
'task_id' => get_post_meta($post->ID, '_igny8_task_id', true),
|
||||
'last_synced' => get_post_meta($post->ID, '_igny8_last_synced', true),
|
||||
'lookup_method' => $lookup_method
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get post status by content_id (DEPRECATED - use get_post_status instead)
|
||||
*
|
||||
* @param WP_REST_Request $request Request object
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function get_post_status_by_content_id($request) {
|
||||
// Redirect to new unified method
|
||||
return $this->get_post_status($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: generate a request_id (UUIDv4 if available)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function generate_request_id() {
|
||||
if (function_exists('wp_generate_uuid4')) {
|
||||
return wp_generate_uuid4();
|
||||
}
|
||||
|
||||
// Fallback: uniqid with more entropy
|
||||
return uniqid('', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Build unified API response and return WP_REST_Response
|
||||
*
|
||||
* @param bool $success
|
||||
* @param mixed $data
|
||||
* @param string|null $message
|
||||
* @param string|null $error
|
||||
* @param array|null $errors
|
||||
* @param int $status
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
private function build_unified_response($success, $data = null, $message = null, $error = null, $errors = null, $status = 200) {
|
||||
$payload = array(
|
||||
'success' => (bool) $success,
|
||||
'data' => $data,
|
||||
'message' => $message,
|
||||
'request_id' => $this->generate_request_id()
|
||||
);
|
||||
|
||||
if (!$success) {
|
||||
$payload['error'] = $error ?: 'Unknown error';
|
||||
if (!empty($errors)) {
|
||||
$payload['errors'] = $errors;
|
||||
}
|
||||
}
|
||||
|
||||
$response = rest_ensure_response($payload);
|
||||
$response->set_status($status);
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /status - Returns plugin connection status and API key info
|
||||
*
|
||||
* @param WP_REST_Request $request
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public function get_status($request) {
|
||||
$api = new Igny8API();
|
||||
$api_key = function_exists('igny8_get_secure_option') ? igny8_get_secure_option('igny8_api_key') : get_option('igny8_api_key');
|
||||
$connection_enabled = igny8_is_connection_enabled();
|
||||
|
||||
$data = array(
|
||||
'connected' => !empty($api_key) && $api->is_authenticated(),
|
||||
'has_api_key' => !empty($api_key),
|
||||
'communication_enabled' => $connection_enabled,
|
||||
'plugin_version' => defined('IGNY8_BRIDGE_VERSION') ? IGNY8_BRIDGE_VERSION : '1.0.0',
|
||||
'wordpress_version' => get_bloginfo('version'),
|
||||
'last_health_check' => get_option('igny8_last_api_health_check', 0),
|
||||
'health' => (!empty($api_key) && $connection_enabled) ? 'healthy' : 'not_configured'
|
||||
);
|
||||
|
||||
return $this->build_unified_response(true, $data, 'Plugin status retrieved', null, null, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /site-metadata/ - returns post types, taxonomies and counts in unified format
|
||||
*
|
||||
* @param WP_REST_Request $request
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public function get_site_metadata($request) {
|
||||
// Use transient cache to avoid expensive counts on large sites
|
||||
$cache_key = 'igny8_site_metadata_v1';
|
||||
$cached = get_transient($cache_key);
|
||||
if ($cached !== false) {
|
||||
return $this->build_unified_response(true, $cached, 'Site metadata (cached)', null, null, 200);
|
||||
}
|
||||
|
||||
// Perform permission check and return unified error if not allowed
|
||||
$perm = $this->check_permission($request);
|
||||
if (is_wp_error($perm)) {
|
||||
$status = 403;
|
||||
$error_data = $perm->get_error_data();
|
||||
if (is_array($error_data) && isset($error_data['status'])) {
|
||||
$status = intval($error_data['status']);
|
||||
}
|
||||
return $this->build_unified_response(false, null, null, $perm->get_error_message(), null, $status);
|
||||
}
|
||||
|
||||
// Collect post types (public)
|
||||
$post_types_objects = get_post_types(array('public' => true), 'objects');
|
||||
$post_types = array();
|
||||
foreach ($post_types_objects as $slug => $obj) {
|
||||
// Get total count across statuses
|
||||
$count_obj = wp_count_posts($slug);
|
||||
$total = 0;
|
||||
if (is_object($count_obj)) {
|
||||
foreach (get_object_vars($count_obj) as $val) {
|
||||
$total += intval($val);
|
||||
}
|
||||
}
|
||||
$post_types[$slug] = array(
|
||||
'label' => $obj->labels->singular_name ?? $obj->label,
|
||||
'count' => $total
|
||||
);
|
||||
}
|
||||
|
||||
// Collect taxonomies (public)
|
||||
$taxonomy_objects = get_taxonomies(array('public' => true), 'objects');
|
||||
$taxonomies = array();
|
||||
foreach ($taxonomy_objects as $slug => $obj) {
|
||||
// Use wp_count_terms when available
|
||||
$term_count = 0;
|
||||
if (function_exists('wp_count_terms')) {
|
||||
$term_count = intval(wp_count_terms($slug));
|
||||
} else {
|
||||
$terms = get_terms(array('taxonomy' => $slug, 'hide_empty' => false, 'fields' => 'ids'));
|
||||
$term_count = is_array($terms) ? count($terms) : 0;
|
||||
}
|
||||
|
||||
$taxonomies[$slug] = array(
|
||||
'label' => $obj->labels->name ?? $obj->label,
|
||||
'count' => $term_count
|
||||
);
|
||||
}
|
||||
|
||||
$data = array(
|
||||
'post_types' => $post_types,
|
||||
'taxonomies' => $taxonomies,
|
||||
'generated_at' => time(),
|
||||
'plugin_connection_enabled' => (bool) igny8_is_connection_enabled(),
|
||||
'two_way_sync_enabled' => (bool) get_option('igny8_enable_two_way_sync', 1)
|
||||
);
|
||||
// Cache for 5 minutes
|
||||
set_transient($cache_key, $data, 300);
|
||||
|
||||
return $this->build_unified_response(true, $data, 'Site metadata retrieved', null, null, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish content to WordPress
|
||||
*
|
||||
* @param WP_REST_Request $request Request object
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function publish_content_to_wordpress($request) {
|
||||
// Check connection
|
||||
if (!igny8_is_connection_enabled()) {
|
||||
return $this->build_unified_response(
|
||||
false,
|
||||
null,
|
||||
'IGNY8 connection is disabled',
|
||||
'connection_disabled',
|
||||
null,
|
||||
403
|
||||
);
|
||||
}
|
||||
|
||||
// DIAGNOSTIC: Log raw request body
|
||||
$raw_body = $request->get_body();
|
||||
error_log('========== RAW REQUEST BODY ==========');
|
||||
error_log($raw_body);
|
||||
error_log('======================================');
|
||||
|
||||
// Get content data from POST body (IGNY8 backend already sends everything)
|
||||
$content_data = $request->get_json_params();
|
||||
|
||||
// DIAGNOSTIC: Log parsed JSON
|
||||
error_log('========== PARSED JSON DATA ==========');
|
||||
error_log(print_r($content_data, true));
|
||||
error_log('======================================');
|
||||
|
||||
// Extract IDs for validation
|
||||
$content_id = isset($content_data['content_id']) ? $content_data['content_id'] : null;
|
||||
$task_id = isset($content_data['task_id']) ? $content_data['task_id'] : null;
|
||||
|
||||
// ALWAYS log incoming data for debugging
|
||||
error_log('========== IGNY8 PUBLISH REQUEST ==========');
|
||||
error_log('Content ID: ' . $content_id);
|
||||
error_log('Task ID: ' . $task_id);
|
||||
error_log('Title: ' . (isset($content_data['title']) ? $content_data['title'] : 'MISSING'));
|
||||
error_log('Content HTML: ' . (isset($content_data['content_html']) ? strlen($content_data['content_html']) . ' chars' : 'MISSING'));
|
||||
error_log('Categories: ' . (isset($content_data['categories']) ? json_encode($content_data['categories']) : 'MISSING'));
|
||||
error_log('Tags: ' . (isset($content_data['tags']) ? json_encode($content_data['tags']) : 'MISSING'));
|
||||
error_log('Featured Image: ' . (isset($content_data['featured_image_url']) ? $content_data['featured_image_url'] : 'MISSING'));
|
||||
error_log('Gallery Images: ' . (isset($content_data['gallery_images']) ? count($content_data['gallery_images']) . ' images' : 'MISSING'));
|
||||
error_log('SEO Title: ' . (isset($content_data['seo_title']) ? 'YES' : 'NO'));
|
||||
error_log('SEO Description: ' . (isset($content_data['seo_description']) ? 'YES' : 'NO'));
|
||||
error_log('Primary Keyword: ' . (isset($content_data['primary_keyword']) ? $content_data['primary_keyword'] : 'MISSING'));
|
||||
error_log('===========================================');
|
||||
|
||||
// Validate required fields
|
||||
if (empty($content_id)) {
|
||||
return $this->build_unified_response(
|
||||
false,
|
||||
null,
|
||||
'Missing content_id in request',
|
||||
'missing_content_id',
|
||||
null,
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($content_data['title'])) {
|
||||
return $this->build_unified_response(
|
||||
false,
|
||||
null,
|
||||
'Missing title in request',
|
||||
'missing_title',
|
||||
null,
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($content_data['content_html'])) {
|
||||
return $this->build_unified_response(
|
||||
false,
|
||||
null,
|
||||
'Missing content_html in request',
|
||||
'missing_content_html',
|
||||
null,
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
// Debug logging
|
||||
if (defined('IGNY8_DEBUG') && IGNY8_DEBUG) {
|
||||
error_log('IGNY8 Publish Request - Content ID: ' . $content_id);
|
||||
error_log('IGNY8 Publish Request - Title: ' . $content_data['title']);
|
||||
error_log('IGNY8 Publish Request - Content HTML length: ' . strlen($content_data['content_html']));
|
||||
}
|
||||
|
||||
// Check if content already exists
|
||||
$existing_posts = get_posts(array(
|
||||
'meta_key' => '_igny8_content_id',
|
||||
'meta_value' => $content_id,
|
||||
'post_type' => 'any',
|
||||
'posts_per_page' => 1
|
||||
));
|
||||
|
||||
if (!empty($existing_posts)) {
|
||||
return $this->build_unified_response(
|
||||
false,
|
||||
array('post_id' => $existing_posts[0]->ID),
|
||||
'Content already exists as WordPress post',
|
||||
'content_exists',
|
||||
null,
|
||||
409
|
||||
);
|
||||
}
|
||||
|
||||
// Create WordPress post
|
||||
$result = igny8_create_wordpress_post_from_task($content_data);
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
return $this->build_unified_response(
|
||||
false,
|
||||
null,
|
||||
'Failed to create WordPress post: ' . $result->get_error_message(),
|
||||
'post_creation_failed',
|
||||
null,
|
||||
500
|
||||
);
|
||||
}
|
||||
|
||||
// Handle new return format (array with post_id and term_ids)
|
||||
if (is_array($result) && isset($result['post_id'])) {
|
||||
$post_id = $result['post_id'];
|
||||
$term_ids = $result['term_ids'] ?? array();
|
||||
} else {
|
||||
// Legacy format (just post_id)
|
||||
$post_id = $result;
|
||||
$term_ids = array();
|
||||
}
|
||||
|
||||
// Return success response with term_ids
|
||||
return $this->build_unified_response(
|
||||
true,
|
||||
array(
|
||||
'post_id' => $post_id,
|
||||
'post_url' => get_permalink($post_id),
|
||||
'post_status' => get_post_status($post_id),
|
||||
'content_id' => $content_id,
|
||||
'task_id' => $task_id,
|
||||
'term_ids' => $term_ids
|
||||
),
|
||||
'Content successfully published to WordPress',
|
||||
null,
|
||||
null,
|
||||
201
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize REST API
|
||||
new Igny8RestAPI();
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Site Integration Class
|
||||
*
|
||||
* Manages site data collection and semantic mapping
|
||||
* Follows WORDPRESS-PLUGIN-INTEGRATION.md guidelines
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Igny8SiteIntegration Class
|
||||
*/
|
||||
class Igny8SiteIntegration {
|
||||
|
||||
/**
|
||||
* API instance
|
||||
*
|
||||
* @var Igny8API
|
||||
*/
|
||||
private $api;
|
||||
|
||||
/**
|
||||
* Site ID
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $site_id;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param int $site_id IGNY8 site ID
|
||||
*/
|
||||
public function __construct($site_id) {
|
||||
$this->api = new Igny8API();
|
||||
$this->site_id = $site_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Full site scan and semantic mapping
|
||||
*
|
||||
* @return array Result array
|
||||
*/
|
||||
public function full_site_scan() {
|
||||
// Collect all data
|
||||
$site_data = igny8_collect_site_data();
|
||||
|
||||
// Send to IGNY8
|
||||
$response = $this->api->post("/system/sites/{$this->site_id}/import/", array(
|
||||
'site_data' => $site_data,
|
||||
'import_type' => 'full_scan'
|
||||
));
|
||||
|
||||
if ($response['success']) {
|
||||
// Map to semantic strategy
|
||||
$mapping = igny8_map_site_to_semantic_strategy($this->site_id, $site_data);
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'import_id' => $response['data']['import_id'] ?? null,
|
||||
'semantic_map' => $mapping['data'] ?? null,
|
||||
'summary' => array(
|
||||
'posts' => count($site_data['posts']),
|
||||
'taxonomies' => count($site_data['taxonomies']),
|
||||
'products' => count($site_data['products'] ?? array()),
|
||||
'product_attributes' => count($site_data['product_attributes'] ?? array())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return array('success' => false, 'error' => $response['error'] ?? 'Unknown error');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get semantic strategy recommendations
|
||||
*
|
||||
* @return array|false Recommendations or false on failure
|
||||
*/
|
||||
public function get_recommendations() {
|
||||
$response = $this->api->get("/planner/sites/{$this->site_id}/recommendations/");
|
||||
|
||||
if ($response['success']) {
|
||||
return $response['data'];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply restructuring recommendations
|
||||
*
|
||||
* @param array $recommendations Recommendations array
|
||||
* @return bool True on success
|
||||
*/
|
||||
public function apply_restructuring($recommendations) {
|
||||
$response = $this->api->post("/planner/sites/{$this->site_id}/restructure/", array(
|
||||
'recommendations' => $recommendations
|
||||
));
|
||||
|
||||
return $response['success'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync incremental site data
|
||||
*
|
||||
* @return array|false Sync result or false on failure
|
||||
*/
|
||||
public function sync_incremental() {
|
||||
return igny8_sync_incremental_site_data($this->site_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* IGNY8 Template Loader
|
||||
*
|
||||
* Loads custom template for IGNY8-generated content
|
||||
* Only applies to posts with _igny8_content_id meta field
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Igny8_Template_Loader {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
// Hook into template loading with high priority
|
||||
add_filter('single_template', [$this, 'load_igny8_template'], 99);
|
||||
|
||||
// Enqueue styles and scripts for IGNY8 template
|
||||
add_action('wp_enqueue_scripts', [$this, 'enqueue_template_assets']);
|
||||
|
||||
// Add body class for IGNY8 content
|
||||
add_filter('body_class', [$this, 'add_body_class']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current post is IGNY8-generated content
|
||||
*
|
||||
* @param int|null $post_id Post ID (optional, defaults to current post)
|
||||
* @return bool
|
||||
*/
|
||||
public function is_igny8_content($post_id = null) {
|
||||
if (!$post_id) {
|
||||
$post_id = get_the_ID();
|
||||
}
|
||||
|
||||
if (!$post_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if post has IGNY8 content ID meta
|
||||
$content_id = get_post_meta($post_id, '_igny8_content_id', true);
|
||||
|
||||
return !empty($content_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load IGNY8 custom template for IGNY8-generated posts
|
||||
*
|
||||
* @param string $template Default template path
|
||||
* @return string Template path
|
||||
*/
|
||||
public function load_igny8_template($template) {
|
||||
global $post;
|
||||
|
||||
// Only apply to single post views
|
||||
if (!is_singular('post')) {
|
||||
return $template;
|
||||
}
|
||||
|
||||
// Only apply to IGNY8-generated content
|
||||
if (!$this->is_igny8_content($post->ID)) {
|
||||
return $template;
|
||||
}
|
||||
|
||||
// Path to our custom template
|
||||
$custom_template = plugin_dir_path(dirname(__FILE__)) . 'templates/single-igny8-content.php';
|
||||
|
||||
// Use custom template if it exists
|
||||
if (file_exists($custom_template)) {
|
||||
return $custom_template;
|
||||
}
|
||||
|
||||
// Fallback to default template
|
||||
return $template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue styles and scripts for IGNY8 template
|
||||
*/
|
||||
public function enqueue_template_assets() {
|
||||
global $post;
|
||||
|
||||
// Only enqueue on single post pages
|
||||
if (!is_singular('post')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only enqueue for IGNY8 content
|
||||
if (!$this->is_igny8_content($post->ID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Enqueue custom styles
|
||||
wp_enqueue_style(
|
||||
'igny8-content-template',
|
||||
plugin_dir_url(dirname(__FILE__)) . 'templates/assets/css/igny8-content-template.css',
|
||||
array(),
|
||||
'1.0.0'
|
||||
);
|
||||
|
||||
// Enqueue custom JavaScript (if needed in future)
|
||||
wp_enqueue_script(
|
||||
'igny8-content-template',
|
||||
plugin_dir_url(dirname(__FILE__)) . 'templates/assets/js/igny8-content-template.js',
|
||||
array('jquery'),
|
||||
'1.0.0',
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add body class for IGNY8 content
|
||||
*
|
||||
* @param array $classes Current body classes
|
||||
* @return array Modified body classes
|
||||
*/
|
||||
public function add_body_class($classes) {
|
||||
global $post;
|
||||
|
||||
if (is_singular('post') && $this->is_igny8_content($post->ID)) {
|
||||
$classes[] = 'igny8-content';
|
||||
$classes[] = 'igny8-template-active';
|
||||
}
|
||||
|
||||
return $classes;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize template loader
|
||||
new Igny8_Template_Loader();
|
||||
@@ -1,147 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Webhook Activity Logs
|
||||
*
|
||||
* Logs webhook activity for auditing and debugging
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log webhook activity
|
||||
*
|
||||
* @param array $data Log data
|
||||
* @return string|false Log ID or false on failure
|
||||
*/
|
||||
function igny8_log_webhook_activity($data) {
|
||||
$logs = get_option('igny8_webhook_logs', array());
|
||||
|
||||
$log_entry = array(
|
||||
'id' => uniqid('webhook_', true),
|
||||
'event' => sanitize_text_field($data['event'] ?? 'unknown'),
|
||||
'data' => $data['data'] ?? null,
|
||||
'ip' => sanitize_text_field($data['ip'] ?? ''),
|
||||
'user_agent' => sanitize_text_field($data['user_agent'] ?? ''),
|
||||
'status' => sanitize_text_field($data['status'] ?? 'received'),
|
||||
'response' => $data['response'] ?? null,
|
||||
'error' => sanitize_text_field($data['error'] ?? ''),
|
||||
'received_at' => current_time('mysql'),
|
||||
'processed_at' => $data['processed_at'] ?? null
|
||||
);
|
||||
|
||||
$logs[] = $log_entry;
|
||||
|
||||
// Keep only last 500 logs
|
||||
if (count($logs) > 500) {
|
||||
$logs = array_slice($logs, -500);
|
||||
}
|
||||
|
||||
update_option('igny8_webhook_logs', $logs);
|
||||
|
||||
return $log_entry['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update webhook log entry
|
||||
*
|
||||
* @param string $log_id Log ID
|
||||
* @param array $updates Updates to apply
|
||||
* @return bool Success
|
||||
*/
|
||||
function igny8_update_webhook_log($log_id, $updates) {
|
||||
$logs = get_option('igny8_webhook_logs', array());
|
||||
|
||||
foreach ($logs as $key => $log) {
|
||||
if ($log['id'] === $log_id) {
|
||||
foreach ($updates as $field => $value) {
|
||||
if ($field === 'status') {
|
||||
$logs[$key][$field] = sanitize_text_field($value);
|
||||
} elseif ($field === 'response') {
|
||||
$logs[$key][$field] = $value;
|
||||
} elseif ($field === 'processed_at') {
|
||||
$logs[$key][$field] = sanitize_text_field($value);
|
||||
} else {
|
||||
$logs[$key][$field] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
update_option('igny8_webhook_logs', $logs);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get webhook logs
|
||||
*
|
||||
* @param array $args Query arguments
|
||||
* @return array Logs
|
||||
*/
|
||||
function igny8_get_webhook_logs($args = array()) {
|
||||
$defaults = array(
|
||||
'limit' => 50,
|
||||
'event' => null,
|
||||
'status' => null
|
||||
);
|
||||
|
||||
$args = wp_parse_args($args, $defaults);
|
||||
$logs = get_option('igny8_webhook_logs', array());
|
||||
|
||||
// Reverse to get newest first
|
||||
$logs = array_reverse($logs);
|
||||
|
||||
// Filter by event
|
||||
if ($args['event']) {
|
||||
$logs = array_filter($logs, function($log) use ($args) {
|
||||
return $log['event'] === $args['event'];
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by status
|
||||
if ($args['status']) {
|
||||
$logs = array_filter($logs, function($log) use ($args) {
|
||||
return $log['status'] === $args['status'];
|
||||
});
|
||||
}
|
||||
|
||||
// Limit results
|
||||
if ($args['limit'] > 0) {
|
||||
$logs = array_slice($logs, 0, $args['limit']);
|
||||
}
|
||||
|
||||
return array_values($logs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear old webhook logs
|
||||
*
|
||||
* @param int $days_old Delete logs older than this many days
|
||||
* @return int Number of logs deleted
|
||||
*/
|
||||
function igny8_clear_old_webhook_logs($days_old = 30) {
|
||||
$logs = get_option('igny8_webhook_logs', array());
|
||||
$cutoff = strtotime("-{$days_old} days");
|
||||
$deleted = 0;
|
||||
|
||||
foreach ($logs as $key => $log) {
|
||||
$log_time = strtotime($log['received_at']);
|
||||
if ($log_time < $cutoff) {
|
||||
unset($logs[$key]);
|
||||
$deleted++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($deleted > 0) {
|
||||
update_option('igny8_webhook_logs', array_values($logs));
|
||||
}
|
||||
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
@@ -1,392 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* IGNY8 Webhooks Handler
|
||||
*
|
||||
* Handles incoming webhooks from IGNY8 SaaS
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Igny8Webhooks Class
|
||||
*/
|
||||
class Igny8Webhooks {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action('rest_api_init', array($this, 'register_webhook_routes'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register webhook REST routes
|
||||
*/
|
||||
public function register_webhook_routes() {
|
||||
// Main webhook endpoint
|
||||
register_rest_route('igny8/v1', '/event', array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array($this, 'handle_webhook'),
|
||||
'permission_callback' => array($this, 'verify_webhook_secret'),
|
||||
'args' => array(
|
||||
'event' => array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'description' => 'Event type'
|
||||
),
|
||||
'data' => array(
|
||||
'required' => true,
|
||||
'type' => 'object',
|
||||
'description' => 'Event data'
|
||||
)
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify webhook API key authentication
|
||||
*
|
||||
* @param WP_REST_Request $request Request object
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
public function verify_webhook_secret($request) {
|
||||
// First check if connection is enabled
|
||||
if (!igny8_is_connection_enabled()) {
|
||||
return new WP_Error(
|
||||
'rest_forbidden',
|
||||
__('IGNY8 connection is disabled', 'igny8-bridge'),
|
||||
array('status' => 403)
|
||||
);
|
||||
}
|
||||
|
||||
// Get API key from plugin settings
|
||||
$stored_api_key = function_exists('igny8_get_secure_option') ? igny8_get_secure_option('igny8_api_key') : get_option('igny8_api_key');
|
||||
|
||||
if (empty($stored_api_key)) {
|
||||
return new WP_Error(
|
||||
'rest_forbidden',
|
||||
__('API key not configured', 'igny8-bridge'),
|
||||
array('status' => 403)
|
||||
);
|
||||
}
|
||||
|
||||
// Check X-IGNY8-API-KEY header
|
||||
$header_api_key = $request->get_header('X-IGNY8-API-KEY');
|
||||
|
||||
// Also check Authorization Bearer header
|
||||
if (empty($header_api_key)) {
|
||||
$auth_header = $request->get_header('Authorization');
|
||||
if ($auth_header && strpos($auth_header, 'Bearer ') === 0) {
|
||||
$header_api_key = substr($auth_header, 7);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($header_api_key)) {
|
||||
igny8_log_webhook_activity(array(
|
||||
'event' => 'authentication_failed',
|
||||
'ip' => $request->get_header('X-Forwarded-For') ?: $request->get_header('Remote-Addr'),
|
||||
'error' => 'Missing API key'
|
||||
));
|
||||
|
||||
return new WP_Error(
|
||||
'rest_forbidden',
|
||||
__('Missing API key in request headers', 'igny8-bridge'),
|
||||
array('status' => 401)
|
||||
);
|
||||
}
|
||||
|
||||
// Verify API key matches
|
||||
if (!hash_equals($stored_api_key, $header_api_key)) {
|
||||
igny8_log_webhook_activity(array(
|
||||
'event' => 'authentication_failed',
|
||||
'ip' => $request->get_header('X-Forwarded-For') ?: $request->get_header('Remote-Addr'),
|
||||
'error' => 'Invalid API key'
|
||||
));
|
||||
|
||||
return new WP_Error(
|
||||
'rest_forbidden',
|
||||
__('Invalid API key', 'igny8-bridge'),
|
||||
array('status' => 401)
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming webhook
|
||||
*
|
||||
* @param WP_REST_Request $request Request object
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function handle_webhook($request) {
|
||||
// Double-check connection is enabled
|
||||
if (!igny8_is_connection_enabled()) {
|
||||
return new WP_Error(
|
||||
'rest_forbidden',
|
||||
__('IGNY8 connection is disabled', 'igny8-bridge'),
|
||||
array('status' => 403)
|
||||
);
|
||||
}
|
||||
|
||||
$event = $request->get_param('event');
|
||||
$data = $request->get_param('data');
|
||||
|
||||
if (empty($event) || empty($data)) {
|
||||
return new WP_Error(
|
||||
'rest_invalid_param',
|
||||
__('Missing event or data parameter', 'igny8-bridge'),
|
||||
array('status' => 400)
|
||||
);
|
||||
}
|
||||
|
||||
// Log webhook receipt
|
||||
$log_id = igny8_log_webhook_activity(array(
|
||||
'event' => $event,
|
||||
'data' => $data,
|
||||
'ip' => $request->get_header('X-Forwarded-For') ?: $request->get_header('Remote-Addr'),
|
||||
'user_agent' => $request->get_header('User-Agent'),
|
||||
'status' => 'received'
|
||||
));
|
||||
|
||||
// Route to appropriate handler
|
||||
$result = null;
|
||||
|
||||
switch ($event) {
|
||||
case 'task_published':
|
||||
case 'task_completed':
|
||||
$result = $this->handle_task_published($data);
|
||||
break;
|
||||
|
||||
case 'link_recommendation':
|
||||
case 'insert_link':
|
||||
$result = $this->handle_link_recommendation($data);
|
||||
break;
|
||||
|
||||
case 'optimizer_request':
|
||||
case 'optimizer_job_completed':
|
||||
$result = $this->handle_optimizer_request($data);
|
||||
break;
|
||||
|
||||
default:
|
||||
$result = array(
|
||||
'success' => false,
|
||||
'error' => 'Unknown event type: ' . $event
|
||||
);
|
||||
}
|
||||
|
||||
// Update log with result
|
||||
if ($log_id) {
|
||||
igny8_update_webhook_log($log_id, array(
|
||||
'status' => $result['success'] ? 'processed' : 'failed',
|
||||
'response' => $result,
|
||||
'processed_at' => current_time('mysql')
|
||||
));
|
||||
}
|
||||
|
||||
return rest_ensure_response($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle task published event
|
||||
*
|
||||
* @param array $data Event data
|
||||
* @return array Result
|
||||
*/
|
||||
private function handle_task_published($data) {
|
||||
if (!igny8_is_connection_enabled()) {
|
||||
return array('success' => false, 'error' => 'Connection disabled');
|
||||
}
|
||||
|
||||
if (function_exists('igny8_is_module_enabled') && !igny8_is_module_enabled('writer')) {
|
||||
return array('success' => false, 'error' => 'Writer module disabled');
|
||||
}
|
||||
|
||||
$task_id = $data['task_id'] ?? null;
|
||||
|
||||
if (!$task_id) {
|
||||
return array('success' => false, 'error' => 'Missing task_id');
|
||||
}
|
||||
|
||||
// Check if post already exists
|
||||
$existing_posts = get_posts(array(
|
||||
'meta_key' => '_igny8_task_id',
|
||||
'meta_value' => $task_id,
|
||||
'post_type' => 'any',
|
||||
'posts_per_page' => 1
|
||||
));
|
||||
|
||||
if (!empty($existing_posts)) {
|
||||
// Post already exists, just update status if needed
|
||||
$post_id = $existing_posts[0]->ID;
|
||||
$status = $data['status'] ?? 'publish';
|
||||
|
||||
if ($status === 'publish' || $status === 'completed') {
|
||||
wp_update_post(array(
|
||||
'ID' => $post_id,
|
||||
'post_status' => 'publish'
|
||||
));
|
||||
}
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'message' => 'Post updated',
|
||||
'post_id' => $post_id
|
||||
);
|
||||
}
|
||||
|
||||
// Fetch full task data and create post
|
||||
$api = new Igny8API();
|
||||
$task_response = $api->get("/writer/tasks/{$task_id}/");
|
||||
|
||||
if (!$task_response['success']) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'error' => 'Failed to fetch task: ' . ($task_response['error'] ?? 'Unknown error')
|
||||
);
|
||||
}
|
||||
|
||||
$task = $task_response['data'];
|
||||
$enabled_post_types = igny8_get_enabled_post_types();
|
||||
|
||||
$content_data = array(
|
||||
'task_id' => $task['id'],
|
||||
'title' => $task['title'] ?? 'Untitled',
|
||||
'content' => $task['content'] ?? '',
|
||||
'status' => $task['status'] ?? 'draft',
|
||||
'cluster_id' => $task['cluster_id'] ?? null,
|
||||
'sector_id' => $task['sector_id'] ?? null,
|
||||
'keyword_ids' => $task['keyword_ids'] ?? array(),
|
||||
'content_type' => $task['content_type'] ?? 'post',
|
||||
'categories' => $task['categories'] ?? array(),
|
||||
'tags' => $task['tags'] ?? array(),
|
||||
'featured_image' => $task['featured_image'] ?? null,
|
||||
'gallery_images' => $task['gallery_images'] ?? array(),
|
||||
'meta_title' => $task['meta_title'] ?? null,
|
||||
'meta_description' => $task['meta_description'] ?? null
|
||||
);
|
||||
|
||||
$post_id = igny8_create_wordpress_post_from_task($content_data, $enabled_post_types);
|
||||
|
||||
if (is_wp_error($post_id)) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'error' => $post_id->get_error_message()
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'message' => 'Post created',
|
||||
'post_id' => $post_id
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle link recommendation event
|
||||
*
|
||||
* @param array $data Event data
|
||||
* @return array Result
|
||||
*/
|
||||
private function handle_link_recommendation($data) {
|
||||
if (!igny8_is_connection_enabled()) {
|
||||
return array('success' => false, 'error' => 'Connection disabled');
|
||||
}
|
||||
|
||||
if (function_exists('igny8_is_module_enabled') && !igny8_is_module_enabled('linker')) {
|
||||
return array('success' => false, 'error' => 'Linker module disabled');
|
||||
}
|
||||
|
||||
$post_id = $data['post_id'] ?? null;
|
||||
$target_url = $data['target_url'] ?? null;
|
||||
$anchor = $data['anchor'] ?? $data['anchor_text'] ?? null;
|
||||
|
||||
if (!$post_id || !$target_url || !$anchor) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'error' => 'Missing required parameters: post_id, target_url, anchor'
|
||||
);
|
||||
}
|
||||
|
||||
// Queue link insertion
|
||||
$queued = igny8_queue_link_insertion(array(
|
||||
'post_id' => intval($post_id),
|
||||
'target_url' => esc_url_raw($target_url),
|
||||
'anchor' => sanitize_text_field($anchor),
|
||||
'source' => 'igny8_linker',
|
||||
'priority' => $data['priority'] ?? 'normal',
|
||||
'created_at' => current_time('mysql')
|
||||
));
|
||||
|
||||
if ($queued) {
|
||||
return array(
|
||||
'success' => true,
|
||||
'message' => 'Link queued for insertion',
|
||||
'queue_id' => $queued
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'success' => false,
|
||||
'error' => 'Failed to queue link insertion'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle optimizer request event
|
||||
*
|
||||
* @param array $data Event data
|
||||
* @return array Result
|
||||
*/
|
||||
private function handle_optimizer_request($data) {
|
||||
if (!igny8_is_connection_enabled()) {
|
||||
return array('success' => false, 'error' => 'Connection disabled');
|
||||
}
|
||||
|
||||
if (function_exists('igny8_is_module_enabled') && !igny8_is_module_enabled('optimizer')) {
|
||||
return array('success' => false, 'error' => 'Optimizer module disabled');
|
||||
}
|
||||
|
||||
$post_id = $data['post_id'] ?? null;
|
||||
$job_id = $data['job_id'] ?? null;
|
||||
$status = $data['status'] ?? null;
|
||||
$score_changes = $data['score_changes'] ?? null;
|
||||
$recommendations = $data['recommendations'] ?? null;
|
||||
|
||||
if (!$post_id) {
|
||||
return array('success' => false, 'error' => 'Missing post_id');
|
||||
}
|
||||
|
||||
// Update optimizer status if job_id provided
|
||||
if ($job_id) {
|
||||
update_post_meta($post_id, '_igny8_optimizer_job_id', $job_id);
|
||||
}
|
||||
|
||||
if ($status) {
|
||||
update_post_meta($post_id, '_igny8_optimizer_status', $status);
|
||||
}
|
||||
|
||||
if ($score_changes) {
|
||||
update_post_meta($post_id, '_igny8_optimizer_score_changes', $score_changes);
|
||||
}
|
||||
|
||||
if ($recommendations) {
|
||||
update_post_meta($post_id, '_igny8_optimizer_recommendations', $recommendations);
|
||||
}
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'message' => 'Optimizer data updated',
|
||||
'post_id' => $post_id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize webhooks
|
||||
new Igny8Webhooks();
|
||||
|
||||
@@ -1,882 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Helper Functions
|
||||
*
|
||||
* WordPress integration functions for IGNY8 Bridge
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get encryption key for secure option storage
|
||||
*
|
||||
* @return string Binary key
|
||||
*/
|
||||
function igny8_get_encryption_key() {
|
||||
$salt = wp_salt('auth');
|
||||
return hash('sha256', 'igny8_bridge_' . $salt, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt a value for storage
|
||||
*
|
||||
* @param string $value Plain text value
|
||||
* @return string Encrypted value with prefix or original value on failure
|
||||
*/
|
||||
function igny8_encrypt_value($value) {
|
||||
if ($value === '' || $value === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!function_exists('openssl_encrypt')) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$iv = openssl_random_pseudo_bytes(16);
|
||||
$cipher = openssl_encrypt($value, 'AES-256-CBC', igny8_get_encryption_key(), OPENSSL_RAW_DATA, $iv);
|
||||
|
||||
if ($cipher === false) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return 'igny8|' . base64_encode($iv . $cipher);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt a stored value
|
||||
*
|
||||
* @param string $value Stored value
|
||||
* @return string Decrypted value or original on failure
|
||||
*/
|
||||
function igny8_decrypt_value($value) {
|
||||
if (!is_string($value) || strpos($value, 'igny8|') !== 0) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (!function_exists('openssl_decrypt')) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$encoded = substr($value, 6);
|
||||
$data = base64_decode($encoded, true);
|
||||
|
||||
if ($data === false || strlen($data) <= 16) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$iv = substr($data, 0, 16);
|
||||
$cipher = substr($data, 16);
|
||||
|
||||
$plain = openssl_decrypt($cipher, 'AES-256-CBC', igny8_get_encryption_key(), OPENSSL_RAW_DATA, $iv);
|
||||
|
||||
return ($plain === false) ? $value : $plain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store an option securely
|
||||
*
|
||||
* @param string $option Option name
|
||||
* @param string $value Value to store
|
||||
*/
|
||||
function igny8_store_secure_option($option, $value) {
|
||||
if ($value === null || $value === '') {
|
||||
delete_option($option);
|
||||
return;
|
||||
}
|
||||
|
||||
update_option($option, igny8_encrypt_value($value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve secure option (with legacy fallback)
|
||||
*
|
||||
* @param string $option Option name
|
||||
* @return string Value
|
||||
*/
|
||||
function igny8_get_secure_option($option) {
|
||||
$stored = get_option($option);
|
||||
|
||||
if (!$stored) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$value = igny8_decrypt_value($stored);
|
||||
|
||||
return is_string($value) ? $value : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get supported post types for automation
|
||||
*
|
||||
* @return array Key => label
|
||||
*/
|
||||
function igny8_get_supported_post_types() {
|
||||
$types = array(
|
||||
'post' => __('Posts', 'igny8-bridge'),
|
||||
'page' => __('Pages', 'igny8-bridge'),
|
||||
);
|
||||
|
||||
if (post_type_exists('product')) {
|
||||
$types['product'] = __('Products', 'igny8-bridge');
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the list of selectable post types.
|
||||
*
|
||||
* @param array $types
|
||||
*/
|
||||
return apply_filters('igny8_supported_post_types', $types);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enabled post types
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function igny8_get_enabled_post_types() {
|
||||
$saved = get_option('igny8_enabled_post_types');
|
||||
|
||||
if (is_array($saved) && !empty($saved)) {
|
||||
return $saved;
|
||||
}
|
||||
|
||||
return array_keys(igny8_get_supported_post_types());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configured control mode
|
||||
*
|
||||
* @return string mirror|hybrid
|
||||
*/
|
||||
function igny8_get_control_mode() {
|
||||
$mode = get_option('igny8_control_mode', 'mirror');
|
||||
return in_array($mode, array('mirror', 'hybrid'), true) ? $mode : 'mirror';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get supported taxonomies for syncing
|
||||
*
|
||||
* @return array Key => label
|
||||
*/
|
||||
function igny8_get_supported_taxonomies() {
|
||||
$taxonomies = array();
|
||||
|
||||
// Standard WordPress taxonomies
|
||||
if (taxonomy_exists('category')) {
|
||||
$taxonomies['category'] = __('Categories', 'igny8-bridge');
|
||||
}
|
||||
|
||||
if (taxonomy_exists('post_tag')) {
|
||||
$taxonomies['post_tag'] = __('Tags', 'igny8-bridge');
|
||||
}
|
||||
|
||||
// WooCommerce taxonomies
|
||||
if (taxonomy_exists('product_cat')) {
|
||||
$taxonomies['product_cat'] = __('Product Categories', 'igny8-bridge');
|
||||
}
|
||||
|
||||
if (taxonomy_exists('product_tag')) {
|
||||
$taxonomies['product_tag'] = __('Product Tags', 'igny8-bridge');
|
||||
}
|
||||
|
||||
if (taxonomy_exists('product_shipping_class')) {
|
||||
$taxonomies['product_shipping_class'] = __('Product Shipping Classes', 'igny8-bridge');
|
||||
}
|
||||
|
||||
// IGNY8 taxonomies (always include)
|
||||
if (taxonomy_exists('igny8_sectors')) {
|
||||
$taxonomies['igny8_sectors'] = __('IGNY8 Sectors', 'igny8-bridge');
|
||||
}
|
||||
|
||||
if (taxonomy_exists('igny8_clusters')) {
|
||||
$taxonomies['igny8_clusters'] = __('IGNY8 Clusters', 'igny8-bridge');
|
||||
}
|
||||
|
||||
// Get custom taxonomies (public only)
|
||||
$custom_taxonomies = get_taxonomies(array(
|
||||
'public' => true,
|
||||
'_builtin' => false
|
||||
), 'objects');
|
||||
|
||||
foreach ($custom_taxonomies as $taxonomy) {
|
||||
// Skip if already added above
|
||||
if (isset($taxonomies[$taxonomy->name])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip post formats and other system taxonomies
|
||||
if (in_array($taxonomy->name, array('post_format', 'wp_theme', 'wp_template_part_area'), true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$taxonomies[$taxonomy->name] = $taxonomy->label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the list of selectable taxonomies.
|
||||
*
|
||||
* @param array $taxonomies
|
||||
*/
|
||||
return apply_filters('igny8_supported_taxonomies', $taxonomies);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enabled taxonomies for syncing
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function igny8_get_enabled_taxonomies() {
|
||||
$saved = get_option('igny8_enabled_taxonomies');
|
||||
|
||||
if (is_array($saved) && !empty($saved)) {
|
||||
return $saved;
|
||||
}
|
||||
|
||||
// Default: enable common taxonomies
|
||||
return array('category', 'post_tag', 'product_cat', 'igny8_sectors', 'igny8_clusters');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a taxonomy is enabled for syncing
|
||||
*
|
||||
* @param string $taxonomy Taxonomy key
|
||||
* @return bool
|
||||
*/
|
||||
function igny8_is_taxonomy_enabled($taxonomy) {
|
||||
$taxonomies = igny8_get_enabled_taxonomies();
|
||||
return in_array($taxonomy, $taxonomies, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available automation modules
|
||||
*
|
||||
* @return array Key => label
|
||||
*/
|
||||
function igny8_get_available_modules() {
|
||||
$modules = array(
|
||||
'sites' => __('Sites (Data & Semantic Map)', 'igny8-bridge'),
|
||||
'planner' => __('Planner (Keywords & Briefs)', 'igny8-bridge'),
|
||||
'writer' => __('Writer (Tasks & Posts)', 'igny8-bridge'),
|
||||
'linker' => __('Linker (Internal Links)', 'igny8-bridge'),
|
||||
'optimizer' => __('Optimizer (Audits & Scores)', 'igny8-bridge'),
|
||||
);
|
||||
|
||||
/**
|
||||
* Filter the list of IGNY8 modules that can be toggled.
|
||||
*
|
||||
* @param array $modules
|
||||
*/
|
||||
return apply_filters('igny8_available_modules', $modules);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enabled modules
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function igny8_get_enabled_modules() {
|
||||
$saved = get_option('igny8_enabled_modules');
|
||||
|
||||
if (is_array($saved) && !empty($saved)) {
|
||||
return $saved;
|
||||
}
|
||||
|
||||
return array_keys(igny8_get_available_modules());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a module is enabled
|
||||
*
|
||||
* @param string $module Module key
|
||||
* @return bool
|
||||
*/
|
||||
function igny8_is_module_enabled($module) {
|
||||
$modules = igny8_get_enabled_modules();
|
||||
return in_array($module, $modules, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a post type is enabled for automation
|
||||
*
|
||||
* @param string $post_type Post type key
|
||||
* @return bool
|
||||
*/
|
||||
function igny8_is_post_type_enabled($post_type) {
|
||||
$post_types = igny8_get_enabled_post_types();
|
||||
return in_array($post_type, $post_types, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if IGNY8 connection is enabled
|
||||
* This is a master switch that disables all sync operations while preserving credentials
|
||||
*
|
||||
* @return bool True if connection is enabled
|
||||
*/
|
||||
if (!function_exists('igny8_log_error')) {
|
||||
function igny8_log_error($message) {
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
error_log('[IGNY8 Plugin] ' . $message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('igny8_is_connection_enabled')) {
|
||||
function igny8_is_connection_enabled() {
|
||||
// Master toggle (defaults to true)
|
||||
$enabled = (bool) get_option('igny8_connection_enabled', 1);
|
||||
|
||||
if (!$enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prefer secure option helpers when available
|
||||
if (function_exists('igny8_get_secure_option')) {
|
||||
$api_key = igny8_get_secure_option('igny8_api_key');
|
||||
} else {
|
||||
$api_key = get_option('igny8_api_key');
|
||||
}
|
||||
|
||||
$site_id = get_option('igny8_site_id');
|
||||
|
||||
if (empty($api_key) || empty($site_id)) {
|
||||
igny8_log_error('Failed to connect to IGNY8 API: API key or Site ID not configured.');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connection state
|
||||
* Three states: not_connected, configured, connected
|
||||
*
|
||||
* @return string Connection state
|
||||
*/
|
||||
function igny8_get_connection_state() {
|
||||
$api_key = function_exists('igny8_get_secure_option') ? igny8_get_secure_option('igny8_api_key') : get_option('igny8_api_key');
|
||||
$integration_id = get_option('igny8_integration_id');
|
||||
$last_structure_sync = get_option('igny8_last_structure_sync');
|
||||
|
||||
if (empty($api_key)) {
|
||||
igny8_log_connection_state('not_connected', 'No API key found');
|
||||
return 'not_connected';
|
||||
}
|
||||
|
||||
if (!empty($api_key) && !empty($integration_id) && !empty($last_structure_sync)) {
|
||||
igny8_log_connection_state('connected', 'Fully connected and synced');
|
||||
return 'connected';
|
||||
}
|
||||
|
||||
igny8_log_connection_state('configured', 'API key set, pending structure sync');
|
||||
return 'configured';
|
||||
}
|
||||
|
||||
/**
|
||||
* Log connection state changes (without exposing API keys)
|
||||
*
|
||||
* @param string $state Connection state
|
||||
* @param string $message Additional context
|
||||
*/
|
||||
function igny8_log_connection_state($state, $message = '') {
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
error_log(sprintf('[IGNY8 Connection] State: %s | %s', $state, $message));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log sync operations (without exposing sensitive data)
|
||||
*
|
||||
* @param string $operation Operation name
|
||||
* @param array $context Context data
|
||||
*/
|
||||
function igny8_log_sync($operation, $context = array()) {
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
// Filter out sensitive keys
|
||||
$safe_context = array_diff_key($context, array_flip(['api_key', 'password', 'secret', 'token']));
|
||||
error_log(sprintf('[IGNY8 Sync] %s | Context: %s', $operation, json_encode($safe_context)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration for site scans
|
||||
*
|
||||
* @param array $overrides Override defaults
|
||||
* @return array
|
||||
*/
|
||||
function igny8_get_site_scan_settings($overrides = array()) {
|
||||
$defaults = array(
|
||||
'post_types' => igny8_get_enabled_post_types(),
|
||||
'include_products' => (bool) get_option('igny8_enable_woocommerce', class_exists('WooCommerce') ? 1 : 0),
|
||||
'per_page' => 100,
|
||||
'since' => null,
|
||||
'mode' => 'full',
|
||||
);
|
||||
|
||||
$settings = wp_parse_args($overrides, $defaults);
|
||||
|
||||
return apply_filters('igny8_site_scan_settings', $settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register IGNY8 post meta fields
|
||||
*/
|
||||
function igny8_register_post_meta() {
|
||||
$post_types = array('post', 'page', 'product');
|
||||
|
||||
// Define all meta fields with proper schema for REST API
|
||||
$meta_fields = array(
|
||||
'_igny8_taxonomy_id' => array(
|
||||
'type' => 'integer',
|
||||
'description' => 'IGNY8 taxonomy ID linked to this post',
|
||||
'single' => true,
|
||||
'show_in_rest' => true,
|
||||
),
|
||||
'_igny8_attribute_id' => array(
|
||||
'type' => 'integer',
|
||||
'description' => 'IGNY8 attribute ID linked to this post',
|
||||
'single' => true,
|
||||
'show_in_rest' => true,
|
||||
),
|
||||
'_igny8_last_synced' => array(
|
||||
'type' => 'string',
|
||||
'description' => 'Last sync timestamp',
|
||||
'single' => true,
|
||||
'show_in_rest' => true,
|
||||
)
|
||||
);
|
||||
|
||||
// Register each meta field for all relevant post types
|
||||
foreach ($meta_fields as $meta_key => $config) {
|
||||
foreach ($post_types as $post_type) {
|
||||
register_post_meta($post_type, $meta_key, $config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register IGNY8 taxonomies
|
||||
*/
|
||||
function igny8_register_taxonomies() {
|
||||
// Register sectors taxonomy (hierarchical) - only if not exists
|
||||
if (!taxonomy_exists('igny8_sectors')) {
|
||||
register_taxonomy('igny8_sectors', array('post', 'page', 'product'), array(
|
||||
'hierarchical' => true,
|
||||
'labels' => array(
|
||||
'name' => 'IGNY8 Sectors',
|
||||
'singular_name' => 'Sector',
|
||||
'menu_name' => 'Sectors',
|
||||
'all_items' => 'All Sectors',
|
||||
'edit_item' => 'Edit Sector',
|
||||
'view_item' => 'View Sector',
|
||||
'update_item' => 'Update Sector',
|
||||
'add_new_item' => 'Add New Sector',
|
||||
'new_item_name' => 'New Sector Name',
|
||||
'parent_item' => 'Parent Sector',
|
||||
'parent_item_colon' => 'Parent Sector:',
|
||||
'search_items' => 'Search Sectors',
|
||||
'not_found' => 'No sectors found',
|
||||
),
|
||||
'public' => true,
|
||||
'show_ui' => true,
|
||||
'show_admin_column' => false,
|
||||
'show_in_nav_menus' => true,
|
||||
'show_tagcloud' => false,
|
||||
'show_in_rest' => true,
|
||||
'rewrite' => array(
|
||||
'slug' => 'sectors',
|
||||
'with_front' => false,
|
||||
),
|
||||
'capabilities' => array(
|
||||
'manage_terms' => 'manage_categories',
|
||||
'edit_terms' => 'manage_categories',
|
||||
'delete_terms' => 'manage_categories',
|
||||
'assign_terms' => 'edit_posts',
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
// Register clusters taxonomy (hierarchical) - only if not exists
|
||||
if (!taxonomy_exists('igny8_clusters')) {
|
||||
register_taxonomy('igny8_clusters', array('post', 'page', 'product'), array(
|
||||
'hierarchical' => true,
|
||||
'labels' => array(
|
||||
'name' => 'IGNY8 Clusters',
|
||||
'singular_name' => 'Cluster',
|
||||
'menu_name' => 'Clusters',
|
||||
'all_items' => 'All Clusters',
|
||||
'edit_item' => 'Edit Cluster',
|
||||
'view_item' => 'View Cluster',
|
||||
'update_item' => 'Update Cluster',
|
||||
'add_new_item' => 'Add New Cluster',
|
||||
'new_item_name' => 'New Cluster Name',
|
||||
'parent_item' => 'Parent Cluster',
|
||||
'parent_item_colon' => 'Parent Cluster:',
|
||||
'search_items' => 'Search Clusters',
|
||||
'not_found' => 'No clusters found',
|
||||
),
|
||||
'public' => true,
|
||||
'show_ui' => true,
|
||||
'show_admin_column' => false,
|
||||
'show_in_nav_menus' => true,
|
||||
'show_tagcloud' => false,
|
||||
'show_in_rest' => true,
|
||||
'rewrite' => array(
|
||||
'slug' => 'clusters',
|
||||
'with_front' => false,
|
||||
),
|
||||
'capabilities' => array(
|
||||
'manage_terms' => 'manage_categories',
|
||||
'edit_terms' => 'manage_categories',
|
||||
'delete_terms' => 'manage_categories',
|
||||
'assign_terms' => 'edit_posts',
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map WordPress post status to IGNY8 task status
|
||||
*
|
||||
* @param string $wp_status WordPress post status
|
||||
* @return string IGNY8 task status
|
||||
*/
|
||||
function igny8_map_wp_status_to_igny8($wp_status) {
|
||||
$status_map = array(
|
||||
'publish' => 'completed',
|
||||
'draft' => 'draft',
|
||||
'pending' => 'pending',
|
||||
'private' => 'completed',
|
||||
'trash' => 'archived',
|
||||
'future' => 'scheduled'
|
||||
);
|
||||
|
||||
return isset($status_map[$wp_status]) ? $status_map[$wp_status] : 'draft';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if post is managed by IGNY8
|
||||
*
|
||||
* @param int $post_id Post ID
|
||||
* @return bool True if IGNY8 managed
|
||||
*/
|
||||
function igny8_is_igny8_managed_post($post_id) {
|
||||
$task_id = get_post_meta($post_id, '_igny8_task_id', true);
|
||||
return !empty($task_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get post data for IGNY8 sync
|
||||
*
|
||||
* @param int $post_id Post ID
|
||||
* @return array|false Post data or false on failure
|
||||
*/
|
||||
function igny8_get_post_data_for_sync($post_id) {
|
||||
$post = get_post($post_id);
|
||||
|
||||
if (!$post) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array(
|
||||
'id' => $post_id,
|
||||
'title' => $post->post_title,
|
||||
'status' => $post->post_status,
|
||||
'url' => get_permalink($post_id),
|
||||
'modified' => $post->post_modified,
|
||||
'published' => $post->post_date,
|
||||
'author' => get_the_author_meta('display_name', $post->post_author),
|
||||
'word_count' => str_word_count(strip_tags($post->post_content)),
|
||||
'meta' => array(
|
||||
'task_id' => get_post_meta($post_id, '_igny8_task_id', true),
|
||||
'content_id' => get_post_meta($post_id, '_igny8_content_id', true),
|
||||
'cluster_id' => get_post_meta($post_id, '_igny8_cluster_id', true),
|
||||
'sector_id' => get_post_meta($post_id, '_igny8_sector_id', true),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule cron jobs
|
||||
*/
|
||||
function igny8_schedule_cron_jobs() {
|
||||
// Schedule daily post status sync (WordPress → IGNY8)
|
||||
if (!wp_next_scheduled('igny8_sync_post_statuses')) {
|
||||
wp_schedule_event(time(), 'daily', 'igny8_sync_post_statuses');
|
||||
}
|
||||
|
||||
// Schedule daily site data sync (incremental)
|
||||
if (!wp_next_scheduled('igny8_sync_site_data')) {
|
||||
wp_schedule_event(time(), 'daily', 'igny8_sync_site_data');
|
||||
}
|
||||
|
||||
// Schedule periodic full site scan (runs at most once per week)
|
||||
if (!wp_next_scheduled('igny8_full_site_scan')) {
|
||||
wp_schedule_event(time(), 'daily', 'igny8_full_site_scan');
|
||||
}
|
||||
|
||||
// Schedule hourly sync from IGNY8 (IGNY8 → WordPress)
|
||||
if (!wp_next_scheduled('igny8_sync_from_igny8')) {
|
||||
wp_schedule_event(time(), 'hourly', 'igny8_sync_from_igny8');
|
||||
}
|
||||
|
||||
// Schedule taxonomy sync
|
||||
if (!wp_next_scheduled('igny8_sync_taxonomies')) {
|
||||
wp_schedule_event(time(), 'twicedaily', 'igny8_sync_taxonomies');
|
||||
}
|
||||
|
||||
// Schedule keyword sync
|
||||
if (!wp_next_scheduled('igny8_sync_keywords')) {
|
||||
wp_schedule_event(time(), 'daily', 'igny8_sync_keywords');
|
||||
}
|
||||
|
||||
// Schedule site structure sync (daily - to keep post types, taxonomies counts up to date)
|
||||
if (!wp_next_scheduled('igny8_sync_site_structure')) {
|
||||
wp_schedule_event(time(), 'daily', 'igny8_sync_site_structure');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unschedule cron jobs
|
||||
*/
|
||||
function igny8_unschedule_cron_jobs() {
|
||||
$timestamp = wp_next_scheduled('igny8_sync_post_statuses');
|
||||
if ($timestamp) {
|
||||
wp_unschedule_event($timestamp, 'igny8_sync_post_statuses');
|
||||
}
|
||||
|
||||
$timestamp = wp_next_scheduled('igny8_sync_site_data');
|
||||
if ($timestamp) {
|
||||
wp_unschedule_event($timestamp, 'igny8_sync_site_data');
|
||||
}
|
||||
|
||||
$timestamp = wp_next_scheduled('igny8_sync_from_igny8');
|
||||
if ($timestamp) {
|
||||
wp_unschedule_event($timestamp, 'igny8_sync_from_igny8');
|
||||
}
|
||||
|
||||
$timestamp = wp_next_scheduled('igny8_full_site_scan');
|
||||
if ($timestamp) {
|
||||
wp_unschedule_event($timestamp, 'igny8_full_site_scan');
|
||||
}
|
||||
|
||||
$timestamp = wp_next_scheduled('igny8_sync_taxonomies');
|
||||
if ($timestamp) {
|
||||
wp_unschedule_event($timestamp, 'igny8_sync_taxonomies');
|
||||
}
|
||||
|
||||
$timestamp = wp_next_scheduled('igny8_sync_keywords');
|
||||
if ($timestamp) {
|
||||
wp_unschedule_event($timestamp, 'igny8_sync_keywords');
|
||||
}
|
||||
|
||||
$timestamp = wp_next_scheduled('igny8_sync_site_structure');
|
||||
if ($timestamp) {
|
||||
wp_unschedule_event($timestamp, 'igny8_sync_site_structure');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WordPress site structure (post types and taxonomies with counts)
|
||||
*
|
||||
* @return array Site structure with post types and taxonomies
|
||||
*/
|
||||
function igny8_get_site_structure() {
|
||||
$post_types_data = array();
|
||||
$taxonomies_data = array();
|
||||
|
||||
// Get all registered post types
|
||||
$post_types = get_post_types(array('public' => true), 'objects');
|
||||
|
||||
foreach ($post_types as $post_type) {
|
||||
// Skip built-in post types we don't care about
|
||||
if (in_array($post_type->name, array('attachment'), true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$count = wp_count_posts($post_type->name);
|
||||
$total = 0;
|
||||
foreach ((array) $count as $status => $num) {
|
||||
if ($status !== 'auto-draft') {
|
||||
$total += (int) $num;
|
||||
}
|
||||
}
|
||||
|
||||
if ($total > 0 || in_array($post_type->name, array('post', 'page', 'product'), true)) {
|
||||
$post_types_data[$post_type->name] = array(
|
||||
'label' => $post_type->label ?: $post_type->name,
|
||||
'count' => $total,
|
||||
'enabled' => igny8_is_post_type_enabled($post_type->name),
|
||||
'fetch_limit' => 100,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Get all registered taxonomies
|
||||
$taxonomies = get_taxonomies(array('public' => true), 'objects');
|
||||
|
||||
foreach ($taxonomies as $taxonomy) {
|
||||
// Skip built-in taxonomies we don't care about
|
||||
if (in_array($taxonomy->name, array('post_format'), true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$terms = get_terms(array(
|
||||
'taxonomy' => $taxonomy->name,
|
||||
'hide_empty' => false,
|
||||
'number' => 0,
|
||||
));
|
||||
|
||||
$count = is_array($terms) ? count($terms) : 0;
|
||||
|
||||
if ($count > 0 || in_array($taxonomy->name, array('category', 'post_tag', 'product_cat'), true)) {
|
||||
$taxonomies_data[$taxonomy->name] = array(
|
||||
'label' => $taxonomy->label ?: $taxonomy->name,
|
||||
'count' => $count,
|
||||
'enabled' => true,
|
||||
'fetch_limit' => 100,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'post_types' => $post_types_data,
|
||||
'taxonomies' => $taxonomies_data,
|
||||
'timestamp' => current_time('c'),
|
||||
);
|
||||
}
|
||||
|
||||
/* Duplicate function removed. See guarded implementation above. */
|
||||
|
||||
/**
|
||||
* Sync WordPress site structure to IGNY8 backend
|
||||
* Called after connection is established
|
||||
*
|
||||
* @return bool True on success, false on failure
|
||||
*/
|
||||
function igny8_sync_site_structure_to_backend($integration_id = null) {
|
||||
// Get site ID from options
|
||||
$site_id = get_option('igny8_site_id');
|
||||
if (!$site_id) {
|
||||
igny8_log_sync('structure_sync_failed', array('reason' => 'No site ID found'));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the site structure
|
||||
$structure = igny8_get_site_structure();
|
||||
if (empty($structure['post_types']) && empty($structure['taxonomies'])) {
|
||||
igny8_log_sync('structure_sync_skipped', array('reason' => 'No post types or taxonomies'));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create a temporary integration object to find the actual integration ID
|
||||
$api = new Igny8API();
|
||||
|
||||
if (!$api->is_authenticated()) {
|
||||
igny8_log_sync('structure_sync_failed', array('reason' => 'Not authenticated'));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use provided integration_id if available, otherwise query for it
|
||||
$integration = null;
|
||||
if ($integration_id) {
|
||||
// Use the provided integration_id directly
|
||||
$integration = array('id' => $integration_id);
|
||||
igny8_log_sync('structure_sync_start', array('integration_id' => $integration_id, 'site_id' => $site_id));
|
||||
} else {
|
||||
// Fallback: Get integration_id from stored option
|
||||
$stored_integration_id = get_option('igny8_integration_id');
|
||||
if ($stored_integration_id) {
|
||||
$integration = array('id' => intval($stored_integration_id));
|
||||
igny8_log_sync('structure_sync_start', array('integration_id' => $stored_integration_id, 'site_id' => $site_id, 'source' => 'stored_option'));
|
||||
} else {
|
||||
// Last resort: Query for integrations
|
||||
$response = $api->get('/v1/integration/integrations/?site=' . $site_id);
|
||||
|
||||
if (!$response['success'] || empty($response['data'])) {
|
||||
igny8_log_sync('structure_sync_failed', array('reason' => 'No integrations found'));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the first integration (should be WordPress integration)
|
||||
if (isset($response['data']['results']) && !empty($response['data']['results'])) {
|
||||
$integration = $response['data']['results'][0];
|
||||
} elseif (is_array($response['data']) && !empty($response['data'])) {
|
||||
$integration = $response['data'][0];
|
||||
}
|
||||
|
||||
if ($integration && !empty($integration['id'])) {
|
||||
// Store integration_id for future use
|
||||
update_option('igny8_integration_id', intval($integration['id']));
|
||||
igny8_log_sync('integration_id_saved', array('integration_id' => $integration['id']));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$integration || empty($integration['id'])) {
|
||||
igny8_log_sync('structure_sync_failed', array('reason' => 'Invalid integration'));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prepare the payload
|
||||
$payload = array(
|
||||
'post_types' => $structure['post_types'],
|
||||
'taxonomies' => $structure['taxonomies'],
|
||||
'timestamp' => $structure['timestamp'],
|
||||
'plugin_connection_enabled' => (bool) igny8_is_connection_enabled(),
|
||||
'two_way_sync_enabled' => (bool) get_option('igny8_enable_two_way_sync', 1),
|
||||
);
|
||||
|
||||
igny8_log_sync('structure_sync_sending', array(
|
||||
'post_types_count' => count($structure['post_types']),
|
||||
'taxonomies_count' => count($structure['taxonomies'])
|
||||
));
|
||||
|
||||
// Send to backend
|
||||
$endpoint = '/v1/integration/integrations/' . $integration['id'] . '/update-structure/';
|
||||
$update_response = $api->post($endpoint, $payload);
|
||||
|
||||
if ($update_response['success']) {
|
||||
igny8_log_sync('structure_sync_success', array(
|
||||
'post_types' => count($structure['post_types']),
|
||||
'taxonomies' => count($structure['taxonomies'])
|
||||
));
|
||||
update_option('igny8_last_structure_sync', current_time('timestamp'));
|
||||
return true;
|
||||
} else {
|
||||
igny8_log_sync('structure_sync_failed', array(
|
||||
'error' => $update_response['error'] ?? 'Unknown error',
|
||||
'http_status' => $update_response['http_status'] ?? 0
|
||||
));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('igny8_handle_rate_limit')) {
|
||||
function igny8_handle_rate_limit($response, $max_retries = 3) {
|
||||
if (isset($response['error']) && strpos($response['error'], 'Rate limit') !== false) {
|
||||
for ($attempt = 0; $attempt < $max_retries; $attempt++) {
|
||||
sleep(pow(2, $attempt)); // Exponential backoff
|
||||
$response = igny8_retry_request(); // Retry logic (to be implemented)
|
||||
if ($response['success']) {
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
igny8_log_error('Max retries exceeded for rate-limited request.');
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('igny8_retry_request')) {
|
||||
function igny8_retry_request() {
|
||||
// Placeholder for retry logic
|
||||
return ['success' => false, 'error' => 'Retry logic not implemented'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Template Helper Functions
|
||||
*
|
||||
* Helper functions for IGNY8 content template
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse content HTML into intro and H2 sections
|
||||
*
|
||||
* @param string $content HTML content
|
||||
* @return array ['intro' => string, 'sections' => array]
|
||||
*/
|
||||
function igny8_parse_content_sections($content) {
|
||||
if (empty($content)) {
|
||||
return ['intro' => '', 'sections' => []];
|
||||
}
|
||||
|
||||
// Use DOMDocument to parse HTML
|
||||
$dom = new DOMDocument('1.0', 'UTF-8');
|
||||
libxml_use_internal_errors(true);
|
||||
|
||||
// Wrap content in a div to ensure proper parsing
|
||||
$wrapped_content = '<div>' . $content . '</div>';
|
||||
$dom->loadHTML('<?xml encoding="UTF-8">' . $wrapped_content, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
||||
libxml_clear_errors();
|
||||
|
||||
$intro_html = '';
|
||||
$sections = [];
|
||||
$current_section = null;
|
||||
|
||||
// Get the wrapper div
|
||||
$xpath = new DOMXPath($dom);
|
||||
$nodes = $xpath->query('//div/*');
|
||||
|
||||
if ($nodes->length === 0) {
|
||||
return ['intro' => $content, 'sections' => []];
|
||||
}
|
||||
|
||||
// Iterate through all child nodes
|
||||
foreach ($nodes as $node) {
|
||||
// Check if node is an H2 heading
|
||||
if ($node->nodeName === 'h2') {
|
||||
// Save previous section if exists
|
||||
if ($current_section !== null) {
|
||||
$sections[] = $current_section;
|
||||
}
|
||||
|
||||
// Start new section
|
||||
$current_section = [
|
||||
'heading' => trim($node->textContent),
|
||||
'content' => '',
|
||||
'id' => sanitize_title($node->textContent)
|
||||
];
|
||||
} elseif ($current_section !== null) {
|
||||
// Add to current section
|
||||
$current_section['content'] .= $dom->saveHTML($node);
|
||||
} else {
|
||||
// Add to intro (before first H2)
|
||||
$intro_html .= $dom->saveHTML($node);
|
||||
}
|
||||
}
|
||||
|
||||
// Save last section
|
||||
if ($current_section !== null) {
|
||||
$sections[] = $current_section;
|
||||
}
|
||||
|
||||
return [
|
||||
'intro' => trim($intro_html),
|
||||
'sections' => $sections
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get in-article images from imported images meta
|
||||
*
|
||||
* @param int $post_id Post ID
|
||||
* @return array Indexed array of image data by position
|
||||
*/
|
||||
function igny8_get_in_article_images($post_id) {
|
||||
$imported_images = get_post_meta($post_id, '_igny8_imported_images', true);
|
||||
|
||||
if (empty($imported_images) || !is_array($imported_images)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$in_article_images = [];
|
||||
|
||||
foreach ($imported_images as $img) {
|
||||
// Skip featured images
|
||||
if (isset($img['is_featured']) && $img['is_featured']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$position = isset($img['position']) ? (int)$img['position'] : count($in_article_images) + 1;
|
||||
$in_article_images[$position] = $img;
|
||||
}
|
||||
|
||||
return $in_article_images;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get featured image prompt from imported images meta
|
||||
*
|
||||
* @param int $post_id Post ID
|
||||
* @return string|null Image prompt or null
|
||||
*/
|
||||
function igny8_get_featured_image_prompt($post_id) {
|
||||
$imported_images = get_post_meta($post_id, '_igny8_imported_images', true);
|
||||
|
||||
if (empty($imported_images) || !is_array($imported_images)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($imported_images as $img) {
|
||||
if (isset($img['is_featured']) && $img['is_featured'] && isset($img['prompt'])) {
|
||||
return $img['prompt'];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format status label for display
|
||||
*
|
||||
* @param string $status Post status
|
||||
* @return string Formatted label
|
||||
*/
|
||||
function igny8_format_status_label($status) {
|
||||
$labels = [
|
||||
'draft' => 'Draft',
|
||||
'pending' => 'Pending Review',
|
||||
'publish' => 'Published',
|
||||
'private' => 'Private',
|
||||
'future' => 'Scheduled',
|
||||
'trash' => 'Trash'
|
||||
];
|
||||
|
||||
return isset($labels[$status]) ? $labels[$status] : ucfirst($status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status CSS class
|
||||
*
|
||||
* @param string $status Post status
|
||||
* @return string CSS class
|
||||
*/
|
||||
function igny8_get_status_class($status) {
|
||||
$classes = [
|
||||
'draft' => 'igny8-status-draft',
|
||||
'pending' => 'igny8-status-pending',
|
||||
'publish' => 'igny8-status-publish',
|
||||
'private' => 'igny8-status-private',
|
||||
'future' => 'igny8-status-future'
|
||||
];
|
||||
|
||||
return isset($classes[$status]) ? $classes[$status] : 'igny8-status-default';
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate word count from content
|
||||
*
|
||||
* @param string $content HTML content
|
||||
* @return int Word count
|
||||
*/
|
||||
function igny8_calculate_word_count($content) {
|
||||
if (empty($content)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Strip HTML tags and count words
|
||||
$text = wp_strip_all_tags($content);
|
||||
return str_word_count($text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse secondary keywords from meta
|
||||
*
|
||||
* @param string $keywords Comma-separated keywords
|
||||
* @return array Array of keywords
|
||||
*/
|
||||
function igny8_parse_keywords($keywords) {
|
||||
if (empty($keywords)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Split by comma and trim each keyword
|
||||
$keywords_array = array_map('trim', explode(',', $keywords));
|
||||
|
||||
// Remove empty values
|
||||
return array_filter($keywords_array);
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
# Copyright (C) 2025 Your Name
|
||||
# This file is distributed under the same license as the IGNY8 WordPress Bridge plugin.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: IGNY8 WordPress Bridge 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/igny8-bridge\n"
|
||||
"POT-Creation-Date: 2025-10-17 12:00+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
|
||||
|
||||
#: admin/settings.php
|
||||
msgid "IGNY8 API Settings"
|
||||
msgstr ""
|
||||
|
||||
#: admin/settings.php
|
||||
msgid "IGNY8 API"
|
||||
msgstr ""
|
||||
|
||||
#: admin/settings.php
|
||||
msgid "API Connection"
|
||||
msgstr ""
|
||||
|
||||
#: admin/settings.php
|
||||
msgid "Email"
|
||||
msgstr ""
|
||||
|
||||
#: admin/settings.php
|
||||
msgid "Your IGNY8 account email address."
|
||||
msgstr ""
|
||||
|
||||
#: admin/settings.php
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#: admin/settings.php
|
||||
msgid "Your IGNY8 account password."
|
||||
msgstr ""
|
||||
|
||||
#: admin/settings.php
|
||||
msgid "Connect to IGNY8"
|
||||
msgstr ""
|
||||
|
||||
#: admin/settings.php
|
||||
msgid "Connection Status"
|
||||
msgstr ""
|
||||
|
||||
#: admin/settings.php
|
||||
msgid "Status"
|
||||
msgstr ""
|
||||
|
||||
#: admin/settings.php
|
||||
msgid "Connected"
|
||||
msgstr ""
|
||||
|
||||
#: admin/settings.php
|
||||
msgid "Site ID"
|
||||
msgstr ""
|
||||
|
||||
#: admin/settings.php
|
||||
msgid "Not Connected"
|
||||
msgstr ""
|
||||
|
||||
#: admin/settings.php
|
||||
msgid "Enter your IGNY8 credentials above and click \"Connect to IGNY8\" to establish a connection."
|
||||
msgstr ""
|
||||
|
||||
#: admin/settings.php
|
||||
msgid "About"
|
||||
msgstr ""
|
||||
|
||||
#: admin/settings.php
|
||||
msgid "The IGNY8 WordPress Bridge plugin connects your WordPress site to the IGNY8 API, enabling two-way synchronization of posts, taxonomies, and site data."
|
||||
msgstr ""
|
||||
|
||||
#: admin/settings.php
|
||||
msgid "Version:"
|
||||
msgstr ""
|
||||
|
||||
#: admin/settings.php
|
||||
msgid "Test Connection"
|
||||
msgstr ""
|
||||
|
||||
#: admin/class-admin.php
|
||||
msgid "Email and password are required."
|
||||
msgstr ""
|
||||
|
||||
#: admin/class-admin.php
|
||||
msgid "Successfully connected to IGNY8 API."
|
||||
msgstr ""
|
||||
|
||||
#: admin/class-admin.php
|
||||
msgid "Failed to connect to IGNY8 API. Please check your credentials."
|
||||
msgstr ""
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* WordPress Hooks Registration
|
||||
*
|
||||
* Registers all WordPress hooks for synchronization
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Load sync class
|
||||
require_once IGNY8_BRIDGE_PLUGIN_DIR . 'sync/post-sync.php';
|
||||
|
||||
/**
|
||||
* Register WordPress hooks for IGNY8 sync
|
||||
*/
|
||||
function igny8_register_sync_hooks() {
|
||||
// WordPress → IGNY8 hooks
|
||||
add_action('save_post', 'igny8_sync_post_status_to_igny8', 10, 3);
|
||||
add_action('publish_post', 'igny8_update_keywords_on_post_publish', 10, 1);
|
||||
add_action('publish_page', 'igny8_update_keywords_on_post_publish', 10, 1);
|
||||
add_action('draft_to_publish', 'igny8_update_keywords_on_post_publish', 10, 1);
|
||||
add_action('future_to_publish', 'igny8_update_keywords_on_post_publish', 10, 1);
|
||||
add_action('transition_post_status', 'igny8_sync_post_status_transition', 10, 3);
|
||||
|
||||
// Cron hooks
|
||||
add_action('igny8_sync_post_statuses', 'igny8_cron_sync_post_statuses');
|
||||
add_action('igny8_sync_site_data', 'igny8_cron_sync_site_data');
|
||||
add_action('igny8_sync_from_igny8', 'igny8_cron_sync_from_igny8');
|
||||
add_action('igny8_sync_taxonomies', 'igny8_cron_sync_taxonomies');
|
||||
add_action('igny8_sync_keywords', 'igny8_cron_sync_keywords');
|
||||
add_action('igny8_full_site_scan', 'igny8_cron_full_site_scan');
|
||||
add_action('igny8_sync_site_structure', 'igny8_sync_site_structure_to_backend');
|
||||
}
|
||||
|
||||
// Register hooks
|
||||
igny8_register_sync_hooks();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,366 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Post Synchronization Functions
|
||||
*
|
||||
* Handles WordPress → IGNY8 post synchronization
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync WordPress post status to IGNY8 when post is saved
|
||||
*
|
||||
* @param int $post_id Post ID
|
||||
* @param WP_Post $post Post object
|
||||
* @param bool $update Whether this is an update
|
||||
*/
|
||||
function igny8_sync_post_status_to_igny8($post_id, $post, $update) {
|
||||
// Skip if connection is disabled
|
||||
if (!igny8_is_connection_enabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip autosaves and revisions
|
||||
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (wp_is_post_revision($post_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only sync IGNY8-managed posts
|
||||
if (!igny8_is_igny8_managed_post($post_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get task ID
|
||||
$task_id = get_post_meta($post_id, '_igny8_task_id', true);
|
||||
if (!$task_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get post status
|
||||
$post_status = $post->post_status;
|
||||
|
||||
// Map WordPress status to IGNY8 task status
|
||||
$task_status = igny8_map_wp_status_to_igny8($post_status);
|
||||
|
||||
// Sync to IGNY8 API
|
||||
$api = new Igny8API();
|
||||
|
||||
// Get content_id if available
|
||||
$content_id = get_post_meta($post_id, '_igny8_content_id', true);
|
||||
|
||||
$update_data = array(
|
||||
'status' => $task_status,
|
||||
'assigned_post_id' => $post_id,
|
||||
'post_url' => get_permalink($post_id),
|
||||
'wordpress_status' => $post_status, // Actual WordPress status
|
||||
'synced_at' => current_time('mysql')
|
||||
);
|
||||
|
||||
// Include content_id if available
|
||||
if ($content_id) {
|
||||
$update_data['content_id'] = $content_id;
|
||||
}
|
||||
|
||||
$response = $api->put("/writer/tasks/{$task_id}/", $update_data);
|
||||
|
||||
if ($response['success']) {
|
||||
// Update WordPress status in meta for IGNY8 to read
|
||||
update_post_meta($post_id, '_igny8_wordpress_status', $post_status);
|
||||
update_post_meta($post_id, '_igny8_last_synced', current_time('mysql'));
|
||||
error_log("IGNY8: Synced post {$post_id} status ({$post_status}) to task {$task_id}");
|
||||
|
||||
// Send status webhook to IGNY8 backend
|
||||
igny8_send_status_webhook($post_id, array('content_id' => $content_id), $post_status);
|
||||
} else {
|
||||
error_log("IGNY8: Failed to sync post status: " . ($response['error'] ?? 'Unknown error'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update keyword status when WordPress post is published
|
||||
*
|
||||
* @param int $post_id Post ID
|
||||
*/
|
||||
function igny8_update_keywords_on_post_publish($post_id) {
|
||||
// Get task ID from post meta
|
||||
$task_id = get_post_meta($post_id, '_igny8_task_id', true);
|
||||
if (!$task_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
$api = new Igny8API();
|
||||
|
||||
// Get task details to find associated cluster/keywords
|
||||
$task_response = $api->get("/writer/tasks/{$task_id}/");
|
||||
|
||||
if (!$task_response['success']) {
|
||||
return;
|
||||
}
|
||||
|
||||
$task = $task_response['data'];
|
||||
$cluster_id = $task['cluster_id'] ?? null;
|
||||
|
||||
if ($cluster_id) {
|
||||
// Get keywords in this cluster
|
||||
$keywords_response = $api->get("/planner/keywords/?cluster_id={$cluster_id}");
|
||||
|
||||
if ($keywords_response['success']) {
|
||||
$keywords = $keywords_response['results'];
|
||||
|
||||
// Update each keyword status to 'mapped'
|
||||
foreach ($keywords as $keyword) {
|
||||
$api->put("/planner/keywords/{$keyword['id']}/", array(
|
||||
'status' => 'mapped'
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update task status to completed
|
||||
$api->put("/writer/tasks/{$task_id}/", array(
|
||||
'status' => 'completed',
|
||||
'assigned_post_id' => $post_id,
|
||||
'post_url' => get_permalink($post_id)
|
||||
));
|
||||
|
||||
update_post_meta($post_id, '_igny8_last_synced', current_time('mysql'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync post status changes to IGNY8
|
||||
*
|
||||
* @param string $new_status New post status
|
||||
* @param string $old_status Old post status
|
||||
* @param WP_Post $post Post object
|
||||
*/
|
||||
function igny8_sync_post_status_transition($new_status, $old_status, $post) {
|
||||
// Skip if connection is disabled
|
||||
if (!igny8_is_connection_enabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if status hasn't changed
|
||||
if ($new_status === $old_status) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only sync IGNY8-managed posts
|
||||
if (!igny8_is_igny8_managed_post($post->ID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$task_id = get_post_meta($post->ID, '_igny8_task_id', true);
|
||||
if (!$task_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
$api = new Igny8API();
|
||||
|
||||
// Map WordPress status to IGNY8 task status
|
||||
$task_status = igny8_map_wp_status_to_igny8($new_status);
|
||||
|
||||
// Sync to IGNY8
|
||||
$response = $api->put("/writer/tasks/{$task_id}/", array(
|
||||
'status' => $task_status,
|
||||
'assigned_post_id' => $post->ID,
|
||||
'post_url' => get_permalink($post->ID)
|
||||
));
|
||||
|
||||
if ($response['success']) {
|
||||
update_post_meta($post->ID, '_igny8_last_synced', current_time('mysql'));
|
||||
do_action('igny8_post_status_synced', $post->ID, $task_id, $new_status);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch sync all IGNY8-managed posts status to IGNY8 API
|
||||
*
|
||||
* @return array Sync results
|
||||
*/
|
||||
function igny8_batch_sync_post_statuses() {
|
||||
// Skip if connection is disabled
|
||||
if (!igny8_is_connection_enabled()) {
|
||||
return array(
|
||||
'synced' => 0,
|
||||
'failed' => 0,
|
||||
'total' => 0,
|
||||
'disabled' => true
|
||||
);
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
// Get all posts with IGNY8 task ID
|
||||
$posts = $wpdb->get_results("
|
||||
SELECT p.ID, p.post_status, p.post_title, pm.meta_value as task_id
|
||||
FROM {$wpdb->posts} p
|
||||
INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
|
||||
WHERE pm.meta_key = '_igny8_task_id'
|
||||
AND p.post_type IN ('post', 'page', 'product')
|
||||
AND p.post_status != 'trash'
|
||||
");
|
||||
|
||||
$api = new Igny8API();
|
||||
$synced = 0;
|
||||
$failed = 0;
|
||||
|
||||
foreach ($posts as $post_data) {
|
||||
$post_id = $post_data->ID;
|
||||
$task_id = intval($post_data->task_id);
|
||||
$wp_status = $post_data->post_status;
|
||||
|
||||
// Map status
|
||||
$task_status = igny8_map_wp_status_to_igny8($wp_status);
|
||||
|
||||
// Sync to IGNY8
|
||||
$response = $api->put("/writer/tasks/{$task_id}/", array(
|
||||
'status' => $task_status,
|
||||
'assigned_post_id' => $post_id,
|
||||
'post_url' => get_permalink($post_id)
|
||||
));
|
||||
|
||||
if ($response['success']) {
|
||||
update_post_meta($post_id, '_igny8_last_synced', current_time('mysql'));
|
||||
$synced++;
|
||||
} else {
|
||||
$failed++;
|
||||
error_log("IGNY8: Failed to sync post {$post_id}: " . ($response['error'] ?? 'Unknown error'));
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'synced' => $synced,
|
||||
'failed' => $failed,
|
||||
'total' => count($posts)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scheduled sync of WordPress post statuses to IGNY8
|
||||
*/
|
||||
function igny8_cron_sync_post_statuses() {
|
||||
// Skip if connection is disabled
|
||||
if (!igny8_is_connection_enabled()) {
|
||||
error_log('IGNY8: Connection disabled, skipping post status sync');
|
||||
return;
|
||||
}
|
||||
|
||||
$result = igny8_batch_sync_post_statuses();
|
||||
|
||||
error_log(sprintf(
|
||||
'IGNY8: Synced %d posts, %d failed',
|
||||
$result['synced'],
|
||||
$result['failed']
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Scheduled sync of site data
|
||||
*/
|
||||
function igny8_cron_sync_site_data() {
|
||||
// Skip if connection is disabled
|
||||
if (!igny8_is_connection_enabled()) {
|
||||
error_log('IGNY8: Connection disabled, skipping site data sync');
|
||||
return;
|
||||
}
|
||||
|
||||
$site_id = get_option('igny8_site_id');
|
||||
|
||||
if (!$site_id) {
|
||||
error_log('IGNY8: Site ID not set, skipping site data sync');
|
||||
return;
|
||||
}
|
||||
|
||||
if (function_exists('igny8_is_module_enabled') && !igny8_is_module_enabled('sites')) {
|
||||
error_log('IGNY8: Sites module disabled, skipping incremental site sync');
|
||||
return;
|
||||
}
|
||||
|
||||
$settings = igny8_get_site_scan_settings(array(
|
||||
'mode' => 'incremental',
|
||||
'since' => get_option('igny8_last_site_sync', 0)
|
||||
));
|
||||
|
||||
$result = igny8_sync_incremental_site_data($site_id, $settings);
|
||||
|
||||
if ($result) {
|
||||
error_log(sprintf(
|
||||
'IGNY8: Synced %d posts to site %d',
|
||||
$result['synced'],
|
||||
$site_id
|
||||
));
|
||||
} else {
|
||||
error_log('IGNY8: Site data sync failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scheduled full site scan (runs at most once per week)
|
||||
*/
|
||||
function igny8_cron_full_site_scan() {
|
||||
// Skip if connection is disabled
|
||||
if (!igny8_is_connection_enabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$site_id = get_option('igny8_site_id');
|
||||
|
||||
if (!$site_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (function_exists('igny8_is_module_enabled') && !igny8_is_module_enabled('sites')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$last_full = intval(get_option('igny8_last_full_site_scan', 0));
|
||||
if ($last_full && (time() - $last_full) < WEEK_IN_SECONDS) {
|
||||
return;
|
||||
}
|
||||
|
||||
$settings = igny8_get_site_scan_settings(array(
|
||||
'mode' => 'full',
|
||||
'since' => null
|
||||
));
|
||||
|
||||
$result = igny8_perform_full_site_scan($site_id, $settings);
|
||||
|
||||
if ($result) {
|
||||
error_log('IGNY8: Full site scan completed');
|
||||
} else {
|
||||
error_log('IGNY8: Full site scan failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Two-way sync class
|
||||
*/
|
||||
class Igny8WordPressSync {
|
||||
|
||||
/**
|
||||
* API instance
|
||||
*
|
||||
* @var Igny8API
|
||||
*/
|
||||
private $api;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->api = new Igny8API();
|
||||
|
||||
// WordPress → IGNY8 hooks are registered in hooks.php
|
||||
// IGNY8 → WordPress hooks can be added here if needed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,425 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Taxonomy Synchronization
|
||||
*
|
||||
* Handles synchronization between WordPress taxonomies and IGNY8 sectors/clusters
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync WordPress taxonomy to IGNY8
|
||||
*
|
||||
* @param string $taxonomy Taxonomy name
|
||||
* @return array|false Sync result or false on failure
|
||||
*/
|
||||
function igny8_sync_taxonomy_to_igny8($taxonomy) {
|
||||
$api = new Igny8API();
|
||||
|
||||
if (!$api->is_authenticated()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$site_id = get_option('igny8_site_id');
|
||||
if (!$site_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get taxonomy data
|
||||
$taxonomy_obj = get_taxonomy($taxonomy);
|
||||
if (!$taxonomy_obj) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get all terms
|
||||
$terms = get_terms(array(
|
||||
'taxonomy' => $taxonomy,
|
||||
'hide_empty' => false
|
||||
));
|
||||
|
||||
if (is_wp_error($terms)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Format taxonomy data
|
||||
$taxonomy_data = array(
|
||||
'name' => $taxonomy,
|
||||
'label' => $taxonomy_obj->label,
|
||||
'hierarchical' => $taxonomy_obj->hierarchical,
|
||||
'terms' => array()
|
||||
);
|
||||
|
||||
foreach ($terms as $term) {
|
||||
$taxonomy_data['terms'][] = array(
|
||||
'id' => $term->term_id,
|
||||
'name' => $term->name,
|
||||
'slug' => $term->slug,
|
||||
'description' => $term->description,
|
||||
'parent' => $term->parent
|
||||
);
|
||||
}
|
||||
|
||||
// Send to IGNY8
|
||||
$response = $api->post("/planner/sites/{$site_id}/taxonomies/", array(
|
||||
'taxonomy' => $taxonomy_data
|
||||
));
|
||||
|
||||
return $response['success'] ? $response['data'] : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync IGNY8 sectors to WordPress taxonomies
|
||||
*
|
||||
* @return array|false Sync result or false on failure
|
||||
*/
|
||||
function igny8_sync_igny8_sectors_to_wp() {
|
||||
// Skip if connection is disabled
|
||||
if (!igny8_is_connection_enabled()) {
|
||||
return array('synced' => 0, 'total' => 0, 'skipped' => true, 'disabled' => true);
|
||||
}
|
||||
|
||||
$api = new Igny8API();
|
||||
|
||||
if (!$api->is_authenticated()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$site_id = get_option('igny8_site_id');
|
||||
if (!$site_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Respect module toggle
|
||||
$enabled_modules = function_exists('igny8_get_enabled_modules') ? igny8_get_enabled_modules() : array();
|
||||
if (!in_array('planner', $enabled_modules, true)) {
|
||||
return array('synced' => 0, 'total' => 0, 'skipped' => true);
|
||||
}
|
||||
|
||||
// Get sectors from IGNY8
|
||||
$response = $api->get("/planner/sites/{$site_id}/sectors/");
|
||||
|
||||
if (!$response['success']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$sectors = $response['data']['results'] ?? $response['data'] ?? $response['results'] ?? array();
|
||||
$synced = 0;
|
||||
|
||||
foreach ($sectors as $sector) {
|
||||
$term = term_exists($sector['name'], 'igny8_sectors');
|
||||
if (!$term) {
|
||||
$term = wp_insert_term(
|
||||
$sector['name'],
|
||||
'igny8_sectors',
|
||||
array(
|
||||
'description' => $sector['description'] ?? '',
|
||||
'slug' => $sector['slug'] ?? sanitize_title($sector['name'])
|
||||
)
|
||||
);
|
||||
} else {
|
||||
wp_update_term($term['term_id'], 'igny8_sectors', array(
|
||||
'description' => $sector['description'] ?? '',
|
||||
'slug' => $sector['slug'] ?? sanitize_title($sector['name'])
|
||||
));
|
||||
}
|
||||
|
||||
if (!is_wp_error($term)) {
|
||||
$term_id = is_array($term) ? $term['term_id'] : $term;
|
||||
update_term_meta($term_id, '_igny8_sector_id', $sector['id']);
|
||||
$synced++;
|
||||
}
|
||||
}
|
||||
|
||||
return array('synced' => $synced, 'total' => count($sectors));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync IGNY8 clusters to WordPress taxonomies
|
||||
*
|
||||
* @param int $sector_id Optional sector ID to filter clusters
|
||||
* @return array|false Sync result or false on failure
|
||||
*/
|
||||
function igny8_sync_igny8_clusters_to_wp($sector_id = null) {
|
||||
// Skip if connection is disabled
|
||||
if (!igny8_is_connection_enabled()) {
|
||||
return array('synced' => 0, 'total' => 0, 'skipped' => true, 'disabled' => true);
|
||||
}
|
||||
|
||||
$api = new Igny8API();
|
||||
|
||||
if (!$api->is_authenticated()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$site_id = get_option('igny8_site_id');
|
||||
if (!$site_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$enabled_modules = function_exists('igny8_get_enabled_modules') ? igny8_get_enabled_modules() : array();
|
||||
if (!in_array('planner', $enabled_modules, true)) {
|
||||
return array('synced' => 0, 'total' => 0, 'skipped' => true);
|
||||
}
|
||||
|
||||
// Build endpoint
|
||||
$endpoint = "/planner/sites/{$site_id}/clusters/";
|
||||
if ($sector_id) {
|
||||
$endpoint .= "?sector_id={$sector_id}";
|
||||
}
|
||||
|
||||
// Get clusters from IGNY8
|
||||
$response = $api->get($endpoint);
|
||||
|
||||
if (!$response['success']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$clusters = $response['data']['results'] ?? $response['data'] ?? $response['results'] ?? array();
|
||||
$synced = 0;
|
||||
|
||||
foreach ($clusters as $cluster) {
|
||||
// Get parent sector term if sector_id exists
|
||||
$parent = 0;
|
||||
if (!empty($cluster['sector_id'])) {
|
||||
$sector_terms = get_terms(array(
|
||||
'taxonomy' => 'igny8_sectors',
|
||||
'meta_key' => '_igny8_sector_id',
|
||||
'meta_value' => $cluster['sector_id'],
|
||||
'hide_empty' => false
|
||||
));
|
||||
|
||||
if (!is_wp_error($sector_terms) && !empty($sector_terms)) {
|
||||
$parent = $sector_terms[0]->term_id;
|
||||
}
|
||||
}
|
||||
|
||||
$term_id = 0;
|
||||
$existing = get_terms(array(
|
||||
'taxonomy' => 'igny8_clusters',
|
||||
'meta_key' => '_igny8_cluster_id',
|
||||
'meta_value' => $cluster['id'],
|
||||
'hide_empty' => false
|
||||
));
|
||||
|
||||
if (!is_wp_error($existing) && !empty($existing)) {
|
||||
$term_id = $existing[0]->term_id;
|
||||
}
|
||||
|
||||
if (!$term_id) {
|
||||
$term = term_exists($cluster['name'], 'igny8_clusters');
|
||||
} else {
|
||||
$term = array('term_id' => $term_id);
|
||||
}
|
||||
if (!$term) {
|
||||
$term = wp_insert_term(
|
||||
$cluster['name'],
|
||||
'igny8_clusters',
|
||||
array(
|
||||
'description' => $cluster['description'] ?? '',
|
||||
'slug' => $cluster['slug'] ?? sanitize_title($cluster['name']),
|
||||
'parent' => $parent
|
||||
)
|
||||
);
|
||||
} else {
|
||||
wp_update_term($term['term_id'], 'igny8_clusters', array(
|
||||
'description' => $cluster['description'] ?? '',
|
||||
'slug' => $cluster['slug'] ?? sanitize_title($cluster['name']),
|
||||
'parent' => $parent
|
||||
));
|
||||
}
|
||||
|
||||
if (!is_wp_error($term)) {
|
||||
$term_id = is_array($term) ? $term['term_id'] : $term;
|
||||
update_term_meta($term_id, '_igny8_cluster_id', $cluster['id']);
|
||||
if (!empty($cluster['sector_id'])) {
|
||||
update_term_meta($term_id, '_igny8_sector_id', $cluster['sector_id']);
|
||||
}
|
||||
$synced++;
|
||||
}
|
||||
}
|
||||
|
||||
return array('synced' => $synced, 'total' => count($clusters));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cron handler: sync sectors/clusters automatically
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function igny8_cron_sync_taxonomies() {
|
||||
// Skip if connection is disabled
|
||||
if (!igny8_is_connection_enabled()) {
|
||||
error_log('IGNY8: Connection disabled, skipping taxonomy sync');
|
||||
return array('sectors' => array('skipped' => true), 'clusters' => array('skipped' => true));
|
||||
}
|
||||
|
||||
$results = array(
|
||||
'sectors' => igny8_sync_igny8_sectors_to_wp(),
|
||||
'clusters' => igny8_sync_igny8_clusters_to_wp()
|
||||
);
|
||||
|
||||
update_option('igny8_last_taxonomy_sync', current_time('timestamp'));
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync planner keywords for all referenced clusters
|
||||
*
|
||||
* @return array|false
|
||||
*/
|
||||
function igny8_sync_keywords_from_planner() {
|
||||
// Skip if connection is disabled
|
||||
if (!igny8_is_connection_enabled()) {
|
||||
return array('synced_clusters' => 0, 'synced_posts' => 0, 'skipped' => true, 'disabled' => true);
|
||||
}
|
||||
|
||||
$api = new Igny8API();
|
||||
|
||||
if (!$api->is_authenticated()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$site_id = get_option('igny8_site_id');
|
||||
if (!$site_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$enabled_modules = function_exists('igny8_get_enabled_modules') ? igny8_get_enabled_modules() : array();
|
||||
if (!in_array('planner', $enabled_modules, true)) {
|
||||
return array('synced_clusters' => 0, 'synced_posts' => 0, 'skipped' => true);
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
$cluster_ids = $wpdb->get_col("
|
||||
SELECT DISTINCT meta_value
|
||||
FROM {$wpdb->postmeta}
|
||||
WHERE meta_key = '_igny8_cluster_id'
|
||||
AND meta_value IS NOT NULL
|
||||
AND meta_value != ''
|
||||
");
|
||||
|
||||
if (empty($cluster_ids)) {
|
||||
return array('synced_clusters' => 0, 'synced_posts' => 0);
|
||||
}
|
||||
|
||||
$enabled_post_types = function_exists('igny8_get_enabled_post_types') ? igny8_get_enabled_post_types() : array('post', 'page');
|
||||
|
||||
$synced_clusters = 0;
|
||||
$synced_posts = 0;
|
||||
|
||||
foreach ($cluster_ids as $cluster_id) {
|
||||
$cluster_id = intval($cluster_id);
|
||||
if (!$cluster_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$response = $api->get("/planner/keywords/?cluster_id={$cluster_id}&page_size=500");
|
||||
if (!$response['success']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$keywords = $response['data']['results'] ?? $response['data'] ?? $response['results'] ?? array();
|
||||
$keyword_ids = array_map('intval', wp_list_pluck($keywords, 'id'));
|
||||
|
||||
// Update cluster term meta
|
||||
$cluster_terms = get_terms(array(
|
||||
'taxonomy' => 'igny8_clusters',
|
||||
'meta_key' => '_igny8_cluster_id',
|
||||
'meta_value' => $cluster_id,
|
||||
'hide_empty' => false
|
||||
));
|
||||
|
||||
if (!is_wp_error($cluster_terms) && !empty($cluster_terms)) {
|
||||
foreach ($cluster_terms as $term) {
|
||||
update_term_meta($term->term_id, '_igny8_keyword_ids', $keyword_ids);
|
||||
}
|
||||
}
|
||||
|
||||
// Update posts tied to this cluster
|
||||
$posts = get_posts(array(
|
||||
'post_type' => $enabled_post_types,
|
||||
'meta_query' => array(
|
||||
array(
|
||||
'key' => '_igny8_cluster_id',
|
||||
'value' => $cluster_id,
|
||||
'compare' => '='
|
||||
)
|
||||
),
|
||||
'post_status' => 'any',
|
||||
'fields' => 'ids',
|
||||
'nopaging' => true
|
||||
));
|
||||
|
||||
foreach ($posts as $post_id) {
|
||||
update_post_meta($post_id, '_igny8_keyword_ids', $keyword_ids);
|
||||
$synced_posts++;
|
||||
}
|
||||
|
||||
$synced_clusters++;
|
||||
}
|
||||
|
||||
return array(
|
||||
'synced_clusters' => $synced_clusters,
|
||||
'synced_posts' => $synced_posts
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cron handler: sync planner keywords
|
||||
*
|
||||
* @return array|false
|
||||
*/
|
||||
function igny8_cron_sync_keywords() {
|
||||
// Skip if connection is disabled
|
||||
if (!igny8_is_connection_enabled()) {
|
||||
error_log('IGNY8: Connection disabled, skipping keyword sync');
|
||||
return array('synced_clusters' => 0, 'synced_posts' => 0, 'skipped' => true);
|
||||
}
|
||||
|
||||
$result = igny8_sync_keywords_from_planner();
|
||||
if ($result !== false) {
|
||||
update_option('igny8_last_keyword_sync', current_time('timestamp'));
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map WordPress taxonomy term to IGNY8 cluster
|
||||
*
|
||||
* @param int $term_id Term ID
|
||||
* @param string $taxonomy Taxonomy name
|
||||
* @param int $cluster_id IGNY8 cluster ID
|
||||
* @return bool True on success
|
||||
*/
|
||||
function igny8_map_term_to_cluster($term_id, $taxonomy, $cluster_id) {
|
||||
if ($taxonomy !== 'igny8_clusters') {
|
||||
return false;
|
||||
}
|
||||
|
||||
update_term_meta($term_id, '_igny8_cluster_id', $cluster_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map WordPress taxonomy to IGNY8 sector
|
||||
*
|
||||
* @param string $taxonomy Taxonomy name
|
||||
* @param int $sector_id IGNY8 sector ID
|
||||
* @return bool True on success
|
||||
*/
|
||||
function igny8_map_taxonomy_to_sector($taxonomy, $sector_id) {
|
||||
// Store mapping in options
|
||||
$mappings = get_option('igny8_taxonomy_sector_mappings', array());
|
||||
$mappings[$taxonomy] = $sector_id;
|
||||
update_option('igny8_taxonomy_sector_mappings', $mappings);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,292 +0,0 @@
|
||||
# IGNY8 Custom Content Template
|
||||
|
||||
## Overview
|
||||
|
||||
The IGNY8 WordPress Bridge plugin now includes a custom post template that automatically applies to IGNY8-generated content, providing a beautiful, magazine-style layout that mirrors the IGNY8 app's content view.
|
||||
|
||||
## Features
|
||||
|
||||
✅ **Automatic Detection** - Only activates for posts with `_igny8_content_id` meta field
|
||||
✅ **Theme Compatible** - Inherits all theme colors and fonts
|
||||
✅ **Responsive Design** - Works beautifully on all devices
|
||||
✅ **Section-Based Layout** - Content parsed into introduction + H2 sections
|
||||
✅ **Image Prompts** - Displays AI image generation prompts
|
||||
✅ **SEO Metadata** - Shows meta title and description
|
||||
✅ **Content Metadata** - Displays clusters, keywords, content type
|
||||
✅ **Professional Typography** - Clean, readable prose styles
|
||||
|
||||
## How It Works
|
||||
|
||||
### Detection
|
||||
|
||||
The template automatically detects IGNY8-generated content by checking for the `_igny8_content_id` post meta field. If found, the custom template is loaded instead of the theme's default `single.php`.
|
||||
|
||||
### Template Structure
|
||||
|
||||
```
|
||||
templates/
|
||||
├── single-igny8-content.php # Main template
|
||||
├── parts/
|
||||
│ ├── igny8-header.php # Title, status, metadata
|
||||
│ ├── igny8-featured-image.php # Featured image with prompt
|
||||
│ ├── igny8-content-sections.php # Parsed content sections
|
||||
│ └── igny8-metadata.php # IGNY8 metadata footer
|
||||
└── assets/
|
||||
├── css/
|
||||
│ └── igny8-content-template.css # Styles
|
||||
└── js/
|
||||
└── igny8-content-template.js # Optional enhancements
|
||||
```
|
||||
|
||||
### Content Parsing
|
||||
|
||||
The template automatically parses your content HTML:
|
||||
|
||||
1. **Introduction** - All content before the first H2 heading
|
||||
2. **Sections** - Each H2 creates a new section with:
|
||||
- Section number badge
|
||||
- Heading
|
||||
- Content
|
||||
- In-article image (if available)
|
||||
|
||||
### Image Display
|
||||
|
||||
- **Featured Image** - Displayed prominently with AI prompt
|
||||
- **In-Article Images** - Positioned next to sections (side-by-side on desktop)
|
||||
- **Image Prompts** - AI generation prompts shown below images
|
||||
|
||||
## Metadata Display
|
||||
|
||||
### Header Section
|
||||
- Post title
|
||||
- Status badge (Draft/Published/etc.)
|
||||
- Posted date
|
||||
- Word count
|
||||
- Author
|
||||
- Categories
|
||||
- Tags
|
||||
- SEO metadata (if different from post title)
|
||||
- Content info (type, structure, cluster, keywords)
|
||||
|
||||
### Footer Section
|
||||
- Collapsible IGNY8 metadata
|
||||
- Content ID, Task ID
|
||||
- Content type and structure
|
||||
- Source
|
||||
- Cluster and Sector IDs
|
||||
- Secondary keywords
|
||||
- Last sync time
|
||||
|
||||
## Theme Compatibility
|
||||
|
||||
The template is designed to work with ANY WordPress theme by:
|
||||
|
||||
### 1. Color Inheritance
|
||||
```css
|
||||
color: inherit; /* Uses theme's text color */
|
||||
background: var(--wp--preset--color--base, #ffffff); /* Uses theme's background */
|
||||
```
|
||||
|
||||
### 2. Font Inheritance
|
||||
```css
|
||||
font-family: inherit; /* Uses theme's font */
|
||||
```
|
||||
|
||||
### 3. Minimal Overrides
|
||||
Only structural styles are applied (spacing, borders, etc.)
|
||||
Colors use opacity overlays: `rgba(0, 0, 0, 0.08)` for neutrals
|
||||
|
||||
### 4. CSS Custom Properties
|
||||
Respects theme's CSS custom properties when available
|
||||
|
||||
## Customization
|
||||
|
||||
### Disable Template
|
||||
|
||||
To disable the custom template and use your theme's default:
|
||||
|
||||
```php
|
||||
// Add to your theme's functions.php
|
||||
add_filter('single_template', function($template) {
|
||||
// Remove IGNY8 template filter
|
||||
remove_filter('single_template', [Igny8_Template_Loader::class, 'load_igny8_template'], 99);
|
||||
return $template;
|
||||
}, 98);
|
||||
```
|
||||
|
||||
### Customize Styles
|
||||
|
||||
You can override styles by adding to your theme's CSS:
|
||||
|
||||
```css
|
||||
/* Override max width */
|
||||
.igny8-content-container {
|
||||
max-width: 1400px;
|
||||
}
|
||||
|
||||
/* Customize section number badge */
|
||||
.igny8-section-number {
|
||||
background: your-theme-color;
|
||||
color: white;
|
||||
}
|
||||
```
|
||||
|
||||
### Modify Template Parts
|
||||
|
||||
You can copy template parts to your theme and modify:
|
||||
|
||||
```
|
||||
your-theme/
|
||||
└── igny8-templates/
|
||||
└── parts/
|
||||
└── igny8-header.php # Your custom header
|
||||
```
|
||||
|
||||
Then filter the template part location:
|
||||
|
||||
```php
|
||||
add_filter('igny8_template_part_path', function($path, $part) {
|
||||
$theme_path = get_stylesheet_directory() . '/igny8-templates/parts/' . $part . '.php';
|
||||
if (file_exists($theme_path)) {
|
||||
return $theme_path;
|
||||
}
|
||||
return $path;
|
||||
}, 10, 2);
|
||||
```
|
||||
|
||||
## Developer Reference
|
||||
|
||||
### Template Detection
|
||||
|
||||
```php
|
||||
// Check if post is IGNY8 content
|
||||
$template_loader = new Igny8_Template_Loader();
|
||||
if ($template_loader->is_igny8_content($post_id)) {
|
||||
// This is IGNY8 content
|
||||
}
|
||||
```
|
||||
|
||||
### Helper Functions
|
||||
|
||||
```php
|
||||
// Parse content into sections
|
||||
$parsed = igny8_parse_content_sections($content_html);
|
||||
// Returns: ['intro' => string, 'sections' => array]
|
||||
|
||||
// Get in-article images
|
||||
$images = igny8_get_in_article_images($post_id);
|
||||
// Returns: array indexed by position
|
||||
|
||||
// Get featured image prompt
|
||||
$prompt = igny8_get_featured_image_prompt($post_id);
|
||||
// Returns: string|null
|
||||
|
||||
// Calculate word count
|
||||
$words = igny8_calculate_word_count($content);
|
||||
// Returns: int
|
||||
|
||||
// Parse keywords
|
||||
$keywords = igny8_parse_keywords($keywords_string);
|
||||
// Returns: array
|
||||
```
|
||||
|
||||
### Hooks & Filters
|
||||
|
||||
```php
|
||||
// Modify template path
|
||||
add_filter('igny8_template_path', function($path) {
|
||||
return $custom_path;
|
||||
});
|
||||
|
||||
// Modify CSS enqueue
|
||||
add_filter('igny8_template_css_url', function($url) {
|
||||
return $custom_url;
|
||||
});
|
||||
|
||||
// Add custom body class
|
||||
add_filter('body_class', function($classes) {
|
||||
if (is_igny8_content()) {
|
||||
$classes[] = 'my-custom-class';
|
||||
}
|
||||
return $classes;
|
||||
});
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Template Not Loading
|
||||
|
||||
1. **Check post meta**: Verify `_igny8_content_id` exists
|
||||
```php
|
||||
$content_id = get_post_meta($post_id, '_igny8_content_id', true);
|
||||
var_dump($content_id); // Should not be empty
|
||||
```
|
||||
|
||||
2. **Check file permissions**: Ensure template files are readable
|
||||
|
||||
3. **Clear cache**: Clear WordPress cache and browser cache
|
||||
|
||||
### Styles Not Applied
|
||||
|
||||
1. **Check enqueue**: Verify CSS is loading in page source
|
||||
2. **Check theme conflicts**: Look for `!important` overrides in theme
|
||||
3. **Check CSS specificity**: IGNY8 styles use minimal specificity
|
||||
|
||||
### Images Not Displaying
|
||||
|
||||
1. **Check meta field**: Verify `_igny8_imported_images` exists and is valid array
|
||||
2. **Check attachment IDs**: Ensure image attachment IDs are valid
|
||||
3. **Check image URLs**: Verify images are accessible
|
||||
|
||||
### Sections Not Parsing
|
||||
|
||||
1. **Check H2 tags**: Content must use `<h2>` for section headings
|
||||
2. **Check HTML structure**: Ensure valid HTML
|
||||
3. **Enable debug**: Add to wp-config.php:
|
||||
```php
|
||||
define('WP_DEBUG', true);
|
||||
define('WP_DEBUG_LOG', true);
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
- **Minimal overhead**: Template only loads for IGNY8 content
|
||||
- **CSS/JS loaded conditionally**: Assets only enqueued when needed
|
||||
- **Efficient parsing**: DOMDocument used for reliable HTML parsing
|
||||
- **No database queries**: All data from post meta (already cached)
|
||||
|
||||
## Browser Support
|
||||
|
||||
- Chrome/Edge (latest)
|
||||
- Firefox (latest)
|
||||
- Safari (latest)
|
||||
- Mobile browsers (iOS Safari, Chrome Mobile)
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Semantic HTML5 elements
|
||||
- Proper heading hierarchy
|
||||
- Alt text for images
|
||||
- Keyboard navigation support
|
||||
- Print styles included
|
||||
|
||||
## License
|
||||
|
||||
GPL v2 or later - Same as WordPress
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
1. Check troubleshooting section above
|
||||
2. Review implementation plan in `/docs/WP-CONTENT-TEMPLATE-IMPLEMENTATION-PLAN.md`
|
||||
3. Check IGNY8 logs in WordPress admin
|
||||
|
||||
## Version History
|
||||
|
||||
### 1.0.0 (December 2025)
|
||||
- Initial release
|
||||
- Custom template for IGNY8 content
|
||||
- Theme-compatible styling
|
||||
- Section-based layout
|
||||
- Image prompt display
|
||||
- SEO metadata display
|
||||
@@ -1,678 +0,0 @@
|
||||
/**
|
||||
* IGNY8 Content Template Styles
|
||||
* Theme-compatible styles using CSS custom properties and inheritance
|
||||
*
|
||||
* Design Philosophy:
|
||||
* - Inherit theme colors and fonts wherever possible
|
||||
* - Use minimal color overrides with opacity for neutral tones
|
||||
* - Responsive and accessible
|
||||
* - Works with any WordPress theme
|
||||
*/
|
||||
|
||||
/* === CSS Variables === */
|
||||
:root {
|
||||
--igny8-max-width: 1200px;
|
||||
--igny8-spacing: 2rem;
|
||||
--igny8-border-radius: 24px;
|
||||
--igny8-border-radius-md: 16px;
|
||||
--igny8-border-radius-sm: 12px;
|
||||
--igny8-border-radius-xs: 8px;
|
||||
}
|
||||
|
||||
/* === Main Wrapper === */
|
||||
.igny8-content-wrapper {
|
||||
padding: var(--igny8-spacing) 0;
|
||||
font-family: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.igny8-content-container {
|
||||
max-width: var(--igny8-max-width);
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
/* === Header Styles === */
|
||||
.igny8-header {
|
||||
background: var(--wp--preset--color--base, #ffffff);
|
||||
border: 2px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: var(--igny8-border-radius);
|
||||
padding: var(--igny8-spacing);
|
||||
margin-bottom: var(--igny8-spacing);
|
||||
box-shadow: 0 4px 16px -4px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.igny8-header-back {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.igny8-back-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.igny8-back-button:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.igny8-back-icon {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.igny8-header-title-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin: 1.5rem 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.igny8-title {
|
||||
font-size: clamp(1.75rem, 4vw, 2.5rem);
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
color: inherit;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.igny8-status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: var(--igny8-border-radius-xs);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.igny8-status-draft {
|
||||
background: rgba(251, 191, 36, 0.15);
|
||||
color: rgba(180, 83, 9, 1);
|
||||
}
|
||||
|
||||
.igny8-status-pending {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: rgba(29, 78, 216, 1);
|
||||
}
|
||||
|
||||
.igny8-status-publish {
|
||||
background: rgba(16, 185, 129, 0.15);
|
||||
color: rgba(5, 150, 105, 1);
|
||||
}
|
||||
|
||||
.igny8-status-private {
|
||||
background: rgba(156, 163, 175, 0.15);
|
||||
color: rgba(75, 85, 99, 1);
|
||||
}
|
||||
|
||||
.igny8-status-future {
|
||||
background: rgba(139, 92, 246, 0.15);
|
||||
color: rgba(109, 40, 217, 1);
|
||||
}
|
||||
|
||||
/* === Metadata Row === */
|
||||
.igny8-metadata-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.igny8-meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.igny8-meta-icon {
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.igny8-meta-label {
|
||||
font-weight: 600;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.igny8-meta-value {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.igny8-meta-badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.igny8-category-badge,
|
||||
.igny8-tag-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: var(--igny8-border-radius-xs);
|
||||
font-size: 0.75rem;
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
color: inherit;
|
||||
line-height: 1.4;
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
/* === SEO Section === */
|
||||
.igny8-seo-section,
|
||||
.igny8-info-section {
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.igny8-seo-header,
|
||||
.igny8-info-header {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
opacity: 0.6;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.igny8-seo-item {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.igny8-seo-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.igny8-seo-label {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
opacity: 0.7;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.igny8-seo-value {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.6;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.igny8-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.igny8-info-item {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.igny8-info-item label {
|
||||
font-weight: 600;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* === Featured Image === */
|
||||
.igny8-featured-image-block {
|
||||
background: var(--wp--preset--color--base, #ffffff);
|
||||
border: 2px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: var(--igny8-border-radius);
|
||||
overflow: hidden;
|
||||
margin-bottom: var(--igny8-spacing);
|
||||
box-shadow: 0 4px 20px -4px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.igny8-featured-header {
|
||||
padding: 2rem 2rem 1rem;
|
||||
}
|
||||
|
||||
.igny8-featured-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.2em;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.igny8-featured-image-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.igny8-featured-image {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.igny8-image-prompt {
|
||||
padding: 1.5rem 2rem;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.igny8-prompt-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.15em;
|
||||
opacity: 0.5;
|
||||
margin: 0 0 0.75rem 0;
|
||||
}
|
||||
|
||||
.igny8-prompt-text {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* === Content Body === */
|
||||
.igny8-content-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3rem;
|
||||
}
|
||||
|
||||
.igny8-intro-section,
|
||||
.igny8-content-section,
|
||||
.igny8-content-fallback {
|
||||
background: var(--wp--preset--color--base, #ffffff);
|
||||
border: 2px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: var(--igny8-border-radius);
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 20px -4px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.igny8-content-section:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 30px -6px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.igny8-intro-section,
|
||||
.igny8-content-fallback {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.igny8-section-label {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.2em;
|
||||
opacity: 0.5;
|
||||
margin-bottom: 0.5rem;
|
||||
display: block;
|
||||
color: rgba(59, 130, 246, 0.8);
|
||||
}
|
||||
|
||||
.igny8-section-container {
|
||||
padding: 2.5rem;
|
||||
}
|
||||
|
||||
.igny8-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.igny8-section-number {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.9), rgba(37, 99, 235, 0.9));
|
||||
color: #ffffff;
|
||||
font-weight: 700;
|
||||
font-size: 1.125rem;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
|
||||
border: 2px solid rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.igny8-section-heading-wrapper {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.igny8-section-heading {
|
||||
font-size: clamp(1.5rem, 3vw, 2rem);
|
||||
font-weight: 700;
|
||||
margin: 0.25rem 0 0 0;
|
||||
color: inherit;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.igny8-section-content {
|
||||
display: grid;
|
||||
gap: 2.5rem;
|
||||
}
|
||||
|
||||
.igny8-section-content.igny8-has-image {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.igny8-section-content.igny8-has-image {
|
||||
grid-template-columns: 3fr 2fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* === Prose Styles === */
|
||||
.igny8-prose {
|
||||
font-size: 1.0625rem;
|
||||
line-height: 1.85;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.igny8-prose h2,
|
||||
.igny8-prose h3,
|
||||
.igny8-prose h4,
|
||||
.igny8-prose h5,
|
||||
.igny8-prose h6 {
|
||||
margin-top: 2.5rem;
|
||||
margin-bottom: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: inherit;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.igny8-prose h2 { font-size: 1.875rem; }
|
||||
.igny8-prose h3 { font-size: 1.5rem; }
|
||||
.igny8-prose h4 { font-size: 1.25rem; }
|
||||
.igny8-prose h5 { font-size: 1.125rem; }
|
||||
.igny8-prose h6 { font-size: 1rem; }
|
||||
|
||||
.igny8-prose p {
|
||||
margin-bottom: 1.3rem;
|
||||
}
|
||||
|
||||
.igny8-prose ul,
|
||||
.igny8-prose ol {
|
||||
margin-bottom: 1.5rem;
|
||||
padding-left: 1.75rem;
|
||||
}
|
||||
|
||||
.igny8-prose li {
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
|
||||
.igny8-prose a {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: rgba(0, 0, 0, 0.3);
|
||||
transition: text-decoration-color 0.2s ease;
|
||||
}
|
||||
|
||||
.igny8-prose a:hover {
|
||||
text-decoration-color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.igny8-prose img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: var(--igny8-border-radius-md);
|
||||
margin: 1.75rem auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.igny8-prose blockquote {
|
||||
margin: 2rem 0;
|
||||
padding: 1.25rem 1.5rem;
|
||||
border-left: 4px solid rgba(59, 130, 246, 0.25);
|
||||
background: rgba(59, 130, 246, 0.08);
|
||||
border-radius: var(--igny8-border-radius-sm);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.igny8-prose table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 2rem 0;
|
||||
border-radius: var(--igny8-border-radius-sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.igny8-prose table th,
|
||||
.igny8-prose table td {
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
padding: 0.875rem 1rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.igny8-prose table th {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.igny8-prose code {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
padding: 0.2rem 0.45rem;
|
||||
border-radius: var(--igny8-border-radius-xs);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.igny8-prose pre {
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
padding: 1.25rem;
|
||||
border-radius: var(--igny8-border-radius-sm);
|
||||
overflow-x: auto;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.igny8-prose hr {
|
||||
border: none;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
margin: 3rem 0;
|
||||
}
|
||||
|
||||
/* === In-Article Images === */
|
||||
.igny8-image-figure {
|
||||
border: 2px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: var(--igny8-border-radius-md);
|
||||
overflow: hidden;
|
||||
background: var(--wp--preset--color--base, #ffffff);
|
||||
margin: 0;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.igny8-in-article-image {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.igny8-image-caption {
|
||||
padding: 1.25rem;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.igny8-caption-label {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.15em;
|
||||
opacity: 0.5;
|
||||
margin: 0 0 0.75rem 0;
|
||||
}
|
||||
|
||||
.igny8-caption-text {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* === Metadata Footer === */
|
||||
.igny8-metadata-footer {
|
||||
margin-top: 3rem;
|
||||
padding: 2rem;
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: var(--igny8-border-radius);
|
||||
}
|
||||
|
||||
.igny8-metadata-summary {
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
user-select: none;
|
||||
list-style: none;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.igny8-metadata-summary::-webkit-details-marker,
|
||||
.igny8-metadata-summary::marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.igny8-metadata-summary::before {
|
||||
content: '▸ ';
|
||||
display: inline-block;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.igny8-metadata-details[open] .igny8-metadata-summary::before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.igny8-metadata-summary:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.igny8-metadata-content {
|
||||
margin-top: 1.5rem;
|
||||
padding: 1.5rem;
|
||||
background: var(--wp--preset--color--base, #ffffff);
|
||||
border: 2px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: var(--igny8-border-radius-xs);
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.igny8-metadata-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.igny8-metadata-table th,
|
||||
.igny8-metadata-table td {
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.igny8-metadata-table th {
|
||||
font-weight: 600;
|
||||
width: 30%;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.igny8-metadata-table tbody tr:last-child th,
|
||||
.igny8-metadata-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.igny8-keywords-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.igny8-keyword-tag {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: var(--igny8-border-radius-xs);
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* === Responsive Styles === */
|
||||
@media (max-width: 768px) {
|
||||
:root {
|
||||
--igny8-spacing: 1.5rem;
|
||||
--igny8-border-radius: 16px;
|
||||
}
|
||||
|
||||
.igny8-header,
|
||||
.igny8-section-container,
|
||||
.igny8-intro-section,
|
||||
.igny8-content-fallback {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.igny8-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.igny8-section-heading {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.igny8-prose {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.igny8-prose h2 { font-size: 1.5rem; }
|
||||
.igny8-prose h3 { font-size: 1.25rem; }
|
||||
.igny8-prose h4 { font-size: 1.125rem; }
|
||||
|
||||
.igny8-metadata-row {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.igny8-info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.igny8-metadata-table th {
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.igny8-header-title-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.igny8-section-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* === Print Styles === */
|
||||
@media print {
|
||||
.igny8-header-back,
|
||||
.igny8-metadata-footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.igny8-header,
|
||||
.igny8-featured-image-block,
|
||||
.igny8-intro-section,
|
||||
.igny8-content-section {
|
||||
box-shadow: none;
|
||||
break-inside: avoid;
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
/**
|
||||
* IGNY8 Content Template JavaScript
|
||||
* Optional JavaScript enhancements
|
||||
*/
|
||||
|
||||
(function($) {
|
||||
'use strict';
|
||||
|
||||
// Wait for DOM ready
|
||||
$(document).ready(function() {
|
||||
|
||||
// Add smooth scroll to section anchors
|
||||
$('.igny8-content-section').each(function() {
|
||||
var sectionId = $(this).attr('id');
|
||||
if (sectionId && window.location.hash === '#' + sectionId) {
|
||||
$('html, body').animate({
|
||||
scrollTop: $(this).offset().top - 100
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Optional: Add copy functionality to code blocks
|
||||
$('.igny8-prose pre code').each(function() {
|
||||
var $code = $(this);
|
||||
var $pre = $code.parent();
|
||||
|
||||
// Add copy button
|
||||
var $copyBtn = $('<button class="igny8-copy-code" title="Copy code">Copy</button>');
|
||||
$pre.css('position', 'relative');
|
||||
$copyBtn.css({
|
||||
'position': 'absolute',
|
||||
'top': '0.5rem',
|
||||
'right': '0.5rem',
|
||||
'padding': '0.25rem 0.75rem',
|
||||
'font-size': '0.75rem',
|
||||
'background': 'rgba(0, 0, 0, 0.1)',
|
||||
'border': 'none',
|
||||
'border-radius': '4px',
|
||||
'cursor': 'pointer'
|
||||
});
|
||||
|
||||
$copyBtn.on('click', function(e) {
|
||||
e.preventDefault();
|
||||
var code = $code.text();
|
||||
|
||||
// Copy to clipboard
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(code).then(function() {
|
||||
$copyBtn.text('Copied!');
|
||||
setTimeout(function() {
|
||||
$copyBtn.text('Copy');
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$pre.append($copyBtn);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})(jQuery);
|
||||
@@ -1,117 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* IGNY8 Content Sections
|
||||
* Parses content HTML and displays sections with in-article images
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Exit if accessed directly
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="igny8-content-body">
|
||||
|
||||
<!-- Introduction (content before first H2) -->
|
||||
<?php if (!empty($parsed_content['intro'])): ?>
|
||||
<section class="igny8-intro-section">
|
||||
<div class="igny8-section-label">Opening Narrative</div>
|
||||
<div class="igny8-prose">
|
||||
<?php echo $parsed_content['intro']; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- H2 Sections with Images -->
|
||||
<?php if (!empty($parsed_content['sections'])): ?>
|
||||
<?php foreach ($parsed_content['sections'] as $index => $section): ?>
|
||||
<section class="igny8-content-section" id="<?php echo esc_attr($section['id']); ?>">
|
||||
<div class="igny8-section-container">
|
||||
|
||||
<div class="igny8-section-header">
|
||||
<span class="igny8-section-number"><?php echo $index + 1; ?></span>
|
||||
<div class="igny8-section-heading-wrapper">
|
||||
<span class="igny8-section-label">Section Spotlight</span>
|
||||
<h2 class="igny8-section-heading"><?php echo esc_html($section['heading']); ?></h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// Get image for this section (position = section index + 1)
|
||||
$section_position = $index + 1;
|
||||
|
||||
// Try multiple sources for in-article images
|
||||
$img_data = null;
|
||||
$img_url = null;
|
||||
$img_prompt = '';
|
||||
|
||||
// Source 1: From $in_article_images array
|
||||
if (isset($in_article_images[$section_position])) {
|
||||
$img_data = $in_article_images[$section_position];
|
||||
if (isset($img_data['attachment_id'])) {
|
||||
$img_url = wp_get_attachment_image_url($img_data['attachment_id'], 'large');
|
||||
$img_prompt = isset($img_data['prompt']) ? $img_data['prompt'] : '';
|
||||
} elseif (isset($img_data['url'])) {
|
||||
$img_url = $img_data['url'];
|
||||
$img_prompt = isset($img_data['prompt']) ? $img_data['prompt'] : '';
|
||||
}
|
||||
}
|
||||
|
||||
// Source 2: Check gallery images meta
|
||||
if (!$img_url) {
|
||||
$gallery = get_post_meta(get_the_ID(), '_igny8_gallery_images', true);
|
||||
if ($gallery && is_array($gallery) && isset($gallery[$index])) {
|
||||
$img_url = wp_get_attachment_image_url($gallery[$index], 'large');
|
||||
}
|
||||
}
|
||||
|
||||
$has_image = !empty($img_url);
|
||||
?>
|
||||
|
||||
<div class="igny8-section-content<?php echo $has_image ? ' igny8-has-image' : ''; ?>">
|
||||
<div class="igny8-section-text">
|
||||
<div class="igny8-prose">
|
||||
<?php echo $section['content']; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($has_image):
|
||||
$img_alt = '';
|
||||
if (isset($img_data['attachment_id'])) {
|
||||
$img_alt = get_post_meta($img_data['attachment_id'], '_wp_attachment_image_alt', true);
|
||||
}
|
||||
?>
|
||||
<div class="igny8-section-image">
|
||||
<figure class="igny8-image-figure">
|
||||
<img src="<?php echo esc_url($img_url); ?>"
|
||||
alt="<?php echo esc_attr($img_alt ?: $section['heading']); ?>"
|
||||
class="igny8-in-article-image"
|
||||
loading="lazy">
|
||||
<?php if ($img_prompt): ?>
|
||||
<figcaption class="igny8-image-caption">
|
||||
<p class="igny8-caption-label">Visual Direction</p>
|
||||
<p class="igny8-caption-text"><?php echo esc_html($img_prompt); ?></p>
|
||||
</figcaption>
|
||||
<?php endif; ?>
|
||||
</figure>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Fallback: If no sections parsed, show content as-is -->
|
||||
<?php if (empty($parsed_content['intro']) && empty($parsed_content['sections'])): ?>
|
||||
<section class="igny8-content-fallback">
|
||||
<div class="igny8-prose">
|
||||
<?php echo $content; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* IGNY8 Featured Image Block
|
||||
* Displays featured image with AI prompt if available
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Exit if accessed directly
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$image_url = wp_get_attachment_image_url($featured_image_id, 'full');
|
||||
$image_alt = get_post_meta($featured_image_id, '_wp_attachment_image_alt', true);
|
||||
|
||||
if (!$image_url) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="igny8-featured-image-block">
|
||||
<div class="igny8-featured-header">
|
||||
<span class="igny8-featured-label">Featured Visual</span>
|
||||
</div>
|
||||
|
||||
<div class="igny8-featured-image-wrapper">
|
||||
<img src="<?php echo esc_url($image_url); ?>"
|
||||
alt="<?php echo esc_attr($image_alt ?: get_the_title()); ?>"
|
||||
class="igny8-featured-image"
|
||||
loading="lazy">
|
||||
</div>
|
||||
|
||||
<?php if ($featured_image_prompt): ?>
|
||||
<div class="igny8-image-prompt">
|
||||
<p class="igny8-prompt-label">AI Image Prompt</p>
|
||||
<p class="igny8-prompt-text"><?php echo esc_html($featured_image_prompt); ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -1,151 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* IGNY8 Content Header
|
||||
* Displays title, status, and metadata
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Exit if accessed directly
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$status_label = igny8_format_status_label($status);
|
||||
$status_class = igny8_get_status_class($status);
|
||||
?>
|
||||
|
||||
<div class="igny8-header">
|
||||
<!-- Back Button -->
|
||||
<div class="igny8-header-back">
|
||||
<a href="<?php echo esc_url(get_post_type_archive_link('post')); ?>" class="igny8-back-button">
|
||||
<span class="igny8-back-icon">←</span>
|
||||
<span>Back to Posts</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Title & Status -->
|
||||
<div class="igny8-header-title-row">
|
||||
<h1 class="igny8-title"><?php the_title(); ?></h1>
|
||||
<span class="igny8-status-badge <?php echo esc_attr($status_class); ?>">
|
||||
<?php echo esc_html($status_label); ?>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Metadata Row -->
|
||||
<div class="igny8-metadata-row">
|
||||
<!-- Created Date -->
|
||||
<div class="igny8-meta-item">
|
||||
<span class="igny8-meta-icon">📅</span>
|
||||
<span class="igny8-meta-label">Posted:</span>
|
||||
<span class="igny8-meta-value"><?php echo get_the_date(); ?></span>
|
||||
</div>
|
||||
|
||||
<!-- Word Count -->
|
||||
<?php if ($word_count > 0): ?>
|
||||
<div class="igny8-meta-item">
|
||||
<span class="igny8-meta-icon">📝</span>
|
||||
<span class="igny8-meta-label">Words:</span>
|
||||
<span class="igny8-meta-value"><?php echo number_format($word_count); ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Author -->
|
||||
<div class="igny8-meta-item">
|
||||
<span class="igny8-meta-icon">✍️</span>
|
||||
<span class="igny8-meta-label">Author:</span>
|
||||
<span class="igny8-meta-value"><?php the_author(); ?></span>
|
||||
</div>
|
||||
|
||||
<!-- Categories -->
|
||||
<?php if ($categories && !is_wp_error($categories)): ?>
|
||||
<div class="igny8-meta-item">
|
||||
<span class="igny8-meta-icon">📁</span>
|
||||
<span class="igny8-meta-label">Categories:</span>
|
||||
<div class="igny8-meta-badges">
|
||||
<?php foreach ($categories as $cat): ?>
|
||||
<span class="igny8-category-badge"><?php echo esc_html($cat->name); ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Tags -->
|
||||
<?php if ($tags && !is_wp_error($tags)): ?>
|
||||
<div class="igny8-meta-item">
|
||||
<span class="igny8-meta-icon">🏷️</span>
|
||||
<span class="igny8-meta-label">Tags:</span>
|
||||
<div class="igny8-meta-badges">
|
||||
<?php foreach ($tags as $tag): ?>
|
||||
<span class="igny8-tag-badge"><?php echo esc_html($tag->name); ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- SEO Metadata Section -->
|
||||
<?php if (($meta_title && $meta_title !== get_the_title()) || $meta_description): ?>
|
||||
<div class="igny8-seo-section">
|
||||
<div class="igny8-seo-header">SEO Metadata</div>
|
||||
|
||||
<?php if ($meta_title && $meta_title !== get_the_title()): ?>
|
||||
<div class="igny8-seo-item">
|
||||
<label class="igny8-seo-label">SEO Title:</label>
|
||||
<div class="igny8-seo-value"><?php echo esc_html($meta_title); ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($meta_description): ?>
|
||||
<div class="igny8-seo-item">
|
||||
<label class="igny8-seo-label">Meta Description:</label>
|
||||
<div class="igny8-seo-value"><?php echo esc_html($meta_description); ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- IGNY8 Content Info -->
|
||||
<?php if ($cluster_name || $primary_keyword || $content_type): ?>
|
||||
<div class="igny8-info-section">
|
||||
<div class="igny8-info-header">Content Information</div>
|
||||
<div class="igny8-info-grid">
|
||||
|
||||
<?php if ($content_type): ?>
|
||||
<div class="igny8-info-item">
|
||||
<label>Type:</label>
|
||||
<span><?php echo esc_html(ucfirst($content_type)); ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($structure): ?>
|
||||
<div class="igny8-info-item">
|
||||
<label>Structure:</label>
|
||||
<span><?php echo esc_html(ucfirst($structure)); ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($cluster_name): ?>
|
||||
<div class="igny8-info-item">
|
||||
<label>Cluster:</label>
|
||||
<span><?php echo esc_html($cluster_name); ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($primary_keyword): ?>
|
||||
<div class="igny8-info-item">
|
||||
<label>Primary Keyword:</label>
|
||||
<span><?php echo esc_html($primary_keyword); ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($source): ?>
|
||||
<div class="igny8-info-item">
|
||||
<label>Source:</label>
|
||||
<span><?php echo esc_html(ucfirst($source)); ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -1,90 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* IGNY8 Metadata Footer
|
||||
* Shows IGNY8-specific metadata in collapsible format
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Exit if accessed directly
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Only show if we have IGNY8 content ID
|
||||
if (!$content_id) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="igny8-metadata-footer">
|
||||
<details class="igny8-metadata-details">
|
||||
<summary class="igny8-metadata-summary">
|
||||
View IGNY8 Metadata
|
||||
</summary>
|
||||
<div class="igny8-metadata-content">
|
||||
<table class="igny8-metadata-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Content ID:</th>
|
||||
<td><?php echo esc_html($content_id); ?></td>
|
||||
</tr>
|
||||
<?php if ($task_id): ?>
|
||||
<tr>
|
||||
<th>Task ID:</th>
|
||||
<td><?php echo esc_html($task_id); ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php if ($content_type): ?>
|
||||
<tr>
|
||||
<th>Content Type:</th>
|
||||
<td><?php echo esc_html(ucfirst($content_type)); ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php if ($structure): ?>
|
||||
<tr>
|
||||
<th>Structure:</th>
|
||||
<td><?php echo esc_html(ucfirst($structure)); ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php if ($source): ?>
|
||||
<tr>
|
||||
<th>Source:</th>
|
||||
<td><?php echo esc_html(ucfirst($source)); ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php if ($cluster_id): ?>
|
||||
<tr>
|
||||
<th>Cluster ID:</th>
|
||||
<td><?php echo esc_html($cluster_id); ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php if ($sector_id): ?>
|
||||
<tr>
|
||||
<th>Sector ID:</th>
|
||||
<td><?php echo esc_html($sector_id); ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php if ($keywords_array): ?>
|
||||
<tr>
|
||||
<th>Secondary Keywords:</th>
|
||||
<td>
|
||||
<div class="igny8-keywords-list">
|
||||
<?php foreach ($keywords_array as $kw): ?>
|
||||
<span class="igny8-keyword-tag"><?php echo esc_html($kw); ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php if ($last_synced): ?>
|
||||
<tr>
|
||||
<th>Last Synced:</th>
|
||||
<td><?php echo esc_html(date('F j, Y, g:i a', strtotime($last_synced))); ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
@@ -1,94 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Single Post Template for IGNY8-Generated Content
|
||||
*
|
||||
* This template is automatically loaded for posts that have _igny8_content_id meta.
|
||||
* It mirrors the design of the IGNY8 app content view template.
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Exit if accessed directly
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Load template functions
|
||||
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/template-functions.php';
|
||||
|
||||
get_header();
|
||||
|
||||
// Get IGNY8 metadata
|
||||
$content_id = get_post_meta(get_the_ID(), '_igny8_content_id', true);
|
||||
$task_id = get_post_meta(get_the_ID(), '_igny8_task_id', true);
|
||||
$content_type = get_post_meta(get_the_ID(), '_igny8_content_type', true);
|
||||
$structure = get_post_meta(get_the_ID(), '_igny8_content_structure', true);
|
||||
$source = get_post_meta(get_the_ID(), '_igny8_source', true);
|
||||
$primary_keyword = get_post_meta(get_the_ID(), '_igny8_primary_keyword', true);
|
||||
$secondary_keywords = get_post_meta(get_the_ID(), '_igny8_secondary_keywords', true);
|
||||
$cluster_name = get_post_meta(get_the_ID(), '_igny8_cluster_name', true);
|
||||
$cluster_id = get_post_meta(get_the_ID(), '_igny8_cluster_id', true);
|
||||
$sector_id = get_post_meta(get_the_ID(), '_igny8_sector_id', true);
|
||||
$meta_title = get_post_meta(get_the_ID(), '_igny8_meta_title', true);
|
||||
$meta_description = get_post_meta(get_the_ID(), '_igny8_meta_description', true);
|
||||
$last_synced = get_post_meta(get_the_ID(), '_igny8_last_synced', true);
|
||||
|
||||
// Parse secondary keywords
|
||||
$keywords_array = igny8_parse_keywords($secondary_keywords);
|
||||
|
||||
// Get WordPress data
|
||||
$categories = get_the_category();
|
||||
$tags = get_the_tags();
|
||||
$word_count = igny8_calculate_word_count(get_the_content());
|
||||
$status = get_post_status();
|
||||
|
||||
// Get images
|
||||
$featured_image_id = get_post_thumbnail_id();
|
||||
$in_article_images = igny8_get_in_article_images(get_the_ID());
|
||||
$featured_image_prompt = igny8_get_featured_image_prompt(get_the_ID());
|
||||
|
||||
// Parse content into sections
|
||||
$content = get_the_content();
|
||||
$content = apply_filters('the_content', $content);
|
||||
$parsed_content = igny8_parse_content_sections($content);
|
||||
|
||||
// Debug logging
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
error_log('IGNY8 Template Debug - Post ID: ' . get_the_ID());
|
||||
error_log('Content length: ' . strlen($content));
|
||||
error_log('Intro length: ' . strlen($parsed_content['intro']));
|
||||
error_log('Sections count: ' . count($parsed_content['sections']));
|
||||
if (!empty($parsed_content['sections'])) {
|
||||
foreach ($parsed_content['sections'] as $idx => $sec) {
|
||||
error_log('Section ' . ($idx + 1) . ': ' . $sec['heading']);
|
||||
}
|
||||
}
|
||||
error_log('In-article images count: ' . count($in_article_images));
|
||||
error_log('Imported images meta: ' . print_r(get_post_meta(get_the_ID(), '_igny8_imported_images', true), true));
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="igny8-content-wrapper">
|
||||
<div class="igny8-content-container">
|
||||
|
||||
<?php
|
||||
// Header with metadata
|
||||
include plugin_dir_path(__FILE__) . 'parts/igny8-header.php';
|
||||
|
||||
// Featured image (if exists)
|
||||
if ($featured_image_id) {
|
||||
include plugin_dir_path(__FILE__) . 'parts/igny8-featured-image.php';
|
||||
}
|
||||
|
||||
// Content sections
|
||||
include plugin_dir_path(__FILE__) . 'parts/igny8-content-sections.php';
|
||||
|
||||
// Metadata footer
|
||||
include plugin_dir_path(__FILE__) . 'parts/igny8-metadata.php';
|
||||
?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
get_footer();
|
||||
@@ -1 +0,0 @@
|
||||
asdasd
|
||||
@@ -1,116 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Test API Authentication
|
||||
*
|
||||
* Tests the fixed API authentication flow.
|
||||
* Run via: php tests/test-api-authentication.php
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Mock WordPress constants and functions if running standalone
|
||||
if (!defined('ABSPATH')) {
|
||||
define('ABSPATH', dirname(dirname(__FILE__)) . '/');
|
||||
}
|
||||
|
||||
// Include plugin files
|
||||
require_once ABSPATH . 'igny8-bridge.php';
|
||||
require_once ABSPATH . 'includes/functions.php';
|
||||
require_once ABSPATH . 'includes/class-igny8-api.php';
|
||||
|
||||
echo "=== IGNY8 API Authentication Test ===\n\n";
|
||||
|
||||
// Test 1: Check if Igny8API class exists
|
||||
echo "Test 1: Check Igny8API class exists\n";
|
||||
if (class_exists('Igny8API')) {
|
||||
echo "✓ PASS: Igny8API class loaded\n\n";
|
||||
} else {
|
||||
echo "✗ FAIL: Igny8API class not found\n\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Test 2: Check Igny8API instantiation
|
||||
echo "Test 2: Instantiate Igny8API\n";
|
||||
try {
|
||||
$api = new Igny8API();
|
||||
echo "✓ PASS: Igny8API instantiated successfully\n\n";
|
||||
} catch (Exception $e) {
|
||||
echo "✗ FAIL: " . $e->getMessage() . "\n\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Test 3: Check is_authenticated method exists
|
||||
echo "Test 3: Check is_authenticated() method\n";
|
||||
if (method_exists($api, 'is_authenticated')) {
|
||||
echo "✓ PASS: is_authenticated() method exists\n";
|
||||
$is_auth = $api->is_authenticated();
|
||||
echo " Status: " . ($is_auth ? "Authenticated" : "Not authenticated") . "\n\n";
|
||||
} else {
|
||||
echo "✗ FAIL: is_authenticated() method not found\n\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Test 4: Check connect method exists
|
||||
echo "Test 4: Check connect() method\n";
|
||||
if (method_exists($api, 'connect')) {
|
||||
echo "✓ PASS: connect() method exists\n\n";
|
||||
} else {
|
||||
echo "✗ FAIL: connect() method not found\n\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Test 5: Check get method exists
|
||||
echo "Test 5: Check get() method\n";
|
||||
if (method_exists($api, 'get')) {
|
||||
echo "✓ PASS: get() method exists\n\n";
|
||||
} else {
|
||||
echo "✗ FAIL: get() method not found\n\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Test 6: Test endpoint path normalization (mock test)
|
||||
echo "Test 6: Endpoint path normalization logic\n";
|
||||
// This is a conceptual test - in reality we'd need to mock HTTP calls
|
||||
echo "✓ PASS: Endpoint normalization should convert:\n";
|
||||
echo " /auth/sites/ → https://api.igny8.com/api/v1/auth/sites/\n";
|
||||
echo " /v1/auth/sites/ → https://api.igny8.com/api/v1/auth/sites/\n";
|
||||
echo " auth/sites → https://api.igny8.com/api/v1/auth/sites/\n\n";
|
||||
|
||||
// Test 7: Check if functions exist
|
||||
echo "Test 7: Check helper functions\n";
|
||||
$functions = array(
|
||||
'igny8_store_secure_option',
|
||||
'igny8_get_secure_option',
|
||||
'igny8_is_connection_enabled'
|
||||
);
|
||||
|
||||
$missing = array();
|
||||
foreach ($functions as $func) {
|
||||
if (!function_exists($func)) {
|
||||
$missing[] = $func;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($missing)) {
|
||||
echo "✓ PASS: All helper functions available\n\n";
|
||||
} else {
|
||||
echo "⚠ WARNING: Missing functions: " . implode(', ', $missing) . "\n";
|
||||
echo " These may be optional depending on WordPress configuration\n\n";
|
||||
}
|
||||
|
||||
// Test 8: Summary
|
||||
echo "=== Test Summary ===\n";
|
||||
echo "✓ API authentication module is properly structured\n";
|
||||
echo "✓ All required methods exist\n";
|
||||
echo "✓ Ready for integration testing\n\n";
|
||||
|
||||
echo "Next Steps:\n";
|
||||
echo "1. Go to WordPress Admin → IGNY8 API Settings\n";
|
||||
echo "2. Get your API key from: https://app.igny8.com/sites/{id}/settings?tab=integrations\n";
|
||||
echo "3. Paste the API key and click 'Connect to IGNY8'\n";
|
||||
echo "4. Check the debug.log for connection details if it fails\n";
|
||||
echo "\nExpected successful response:\n";
|
||||
echo " Authorization: Bearer sk_live_xxxxxxxxxxxxx\n";
|
||||
echo " GET https://api.igny8.com/api/v1/auth/sites/\n";
|
||||
echo " Response: {\"success\": true, \"data\": [{\"id\": 1, ...}]}\n";
|
||||
?>
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Unit test for API key revoke handler
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
class Test_Revoke_Api_Key extends WP_UnitTestCase {
|
||||
|
||||
public function test_revoke_api_key_clears_options() {
|
||||
// Simulate stored API key and tokens
|
||||
update_option('igny8_api_key', 'test-key-123');
|
||||
update_option('igny8_access_token', 'test-key-123');
|
||||
update_option('igny8_refresh_token', 'refresh-123');
|
||||
update_option('igny8_token_refreshed_at', time());
|
||||
|
||||
// Call revoke
|
||||
Igny8Admin::revoke_api_key();
|
||||
|
||||
// Assert removed
|
||||
$this->assertFalse(get_option('igny8_api_key'));
|
||||
$this->assertFalse(get_option('igny8_access_token'));
|
||||
$this->assertFalse(get_option('igny8_refresh_token'));
|
||||
$this->assertFalse(get_option('igny8_token_refreshed_at'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Basic unit test for site-metadata endpoint
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
class Test_Site_Metadata_Endpoint extends WP_UnitTestCase {
|
||||
|
||||
public function test_site_metadata_endpoint_returns_success() {
|
||||
// Ensure connection enabled
|
||||
update_option('igny8_connection_enabled', 1);
|
||||
|
||||
// Create a fake API key so permission checks pass via Igny8API
|
||||
update_option('igny8_api_key', 'test-api-key-123');
|
||||
update_option('igny8_access_token', 'test-api-key-123');
|
||||
|
||||
// Build request
|
||||
$request = new WP_REST_Request('GET', '/igny8/v1/site-metadata/');
|
||||
$request->set_header('Authorization', 'Bearer test-api-key-123');
|
||||
|
||||
$server = rest_get_server();
|
||||
$response = $server->dispatch($request);
|
||||
|
||||
$this->assertEquals(200, $response->get_status());
|
||||
$data = $response->get_data();
|
||||
$this->assertNotEmpty($data);
|
||||
$this->assertArrayHasKey('success', $data);
|
||||
$this->assertTrue($data['success']);
|
||||
$this->assertArrayHasKey('data', $data);
|
||||
$this->assertArrayHasKey('post_types', $data['data']);
|
||||
$this->assertArrayHasKey('taxonomies', $data['data']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Test Site Structure Sync
|
||||
*
|
||||
* Run this test to verify site structure sync is working correctly
|
||||
* Usage: wp eval-file tests/test-sync-structure.php
|
||||
* Or: http://your-site.com/wp-admin/admin-ajax.php?action=igny8_test_structure_sync
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
echo "=== IGNY8 Site Structure Sync Test ===\n\n";
|
||||
|
||||
// Test 1: Check site ID
|
||||
echo "Test 1: Checking Site ID...\n";
|
||||
$site_id = get_option('igny8_site_id');
|
||||
if ($site_id) {
|
||||
echo "✅ Site ID found: $site_id\n";
|
||||
} else {
|
||||
echo "❌ Site ID not found. Run connection setup first.\n";
|
||||
exit;
|
||||
}
|
||||
echo "\n";
|
||||
|
||||
// Test 2: Check authentication
|
||||
echo "Test 2: Checking Authentication...\n";
|
||||
$api = new Igny8API();
|
||||
if ($api->is_authenticated()) {
|
||||
echo "✅ API is authenticated\n";
|
||||
} else {
|
||||
echo "❌ API is not authenticated. Credentials missing.\n";
|
||||
exit;
|
||||
}
|
||||
echo "\n";
|
||||
|
||||
// Test 3: Get site structure
|
||||
echo "Test 3: Gathering Site Structure...\n";
|
||||
$structure = igny8_get_site_structure();
|
||||
|
||||
echo " Post Types Found: " . count($structure['post_types']) . "\n";
|
||||
foreach ($structure['post_types'] as $type => $data) {
|
||||
echo " - $type: {$data['label']} ({$data['count']} items)\n";
|
||||
}
|
||||
|
||||
echo "\n Taxonomies Found: " . count($structure['taxonomies']) . "\n";
|
||||
foreach ($structure['taxonomies'] as $tax => $data) {
|
||||
echo " - $tax: {$data['label']} ({$data['count']} items)\n";
|
||||
}
|
||||
|
||||
if (empty($structure['post_types']) && empty($structure['taxonomies'])) {
|
||||
echo "❌ No content found to sync\n";
|
||||
exit;
|
||||
}
|
||||
|
||||
echo "✅ Site structure gathered successfully\n";
|
||||
echo "\n";
|
||||
|
||||
// Test 4: Query for integration
|
||||
echo "Test 4: Querying for Integration...\n";
|
||||
$query_response = $api->get('/v1/integration/integrations/?site=' . $site_id . '&platform=wordpress');
|
||||
|
||||
echo " API Response Status: " . ($query_response['success'] ? 'Success' : 'Failed') . "\n";
|
||||
echo " HTTP Status: " . (isset($query_response['http_status']) ? $query_response['http_status'] : 'N/A') . "\n";
|
||||
|
||||
// Extract integration
|
||||
$integration = null;
|
||||
if (isset($query_response['data'])) {
|
||||
$data = $query_response['data'];
|
||||
|
||||
if (isset($data['results']) && !empty($data['results'])) {
|
||||
$integration = $data['results'][0];
|
||||
echo " Response Format: Paginated (DRF)\n";
|
||||
} elseif (is_array($data) && isset($data[0])) {
|
||||
$integration = $data[0];
|
||||
echo " Response Format: Direct Array\n";
|
||||
} elseif (is_array($data) && isset($data['id'])) {
|
||||
$integration = $data;
|
||||
echo " Response Format: Single Object\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (!$integration || empty($integration['id'])) {
|
||||
echo "❌ No integration found\n";
|
||||
if (isset($query_response['error'])) {
|
||||
echo " Error: " . $query_response['error'] . "\n";
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
echo "✅ Integration found: ID {$integration['id']}\n";
|
||||
echo "\n";
|
||||
|
||||
// Test 5: Sync structure to backend
|
||||
echo "Test 5: Syncing Structure to Backend...\n";
|
||||
|
||||
$payload = array(
|
||||
'post_types' => $structure['post_types'],
|
||||
'taxonomies' => $structure['taxonomies'],
|
||||
'timestamp' => $structure['timestamp'],
|
||||
'plugin_connection_enabled' => (bool) igny8_is_connection_enabled(),
|
||||
'two_way_sync_enabled' => (bool) get_option('igny8_enable_two_way_sync', 1),
|
||||
);
|
||||
|
||||
$endpoint = '/v1/integration/integrations/' . $integration['id'] . '/update-structure/';
|
||||
echo " Endpoint: $endpoint\n";
|
||||
echo " Payload Size: " . strlen(json_encode($payload)) . " bytes\n";
|
||||
|
||||
$sync_response = $api->post($endpoint, $payload);
|
||||
|
||||
echo " API Response Status: " . ($sync_response['success'] ? 'Success' : 'Failed') . "\n";
|
||||
echo " HTTP Status: " . (isset($sync_response['http_status']) ? $sync_response['http_status'] : 'N/A') . "\n";
|
||||
|
||||
if ($sync_response['success']) {
|
||||
echo "✅ Structure synced successfully\n";
|
||||
if (isset($sync_response['data']['message'])) {
|
||||
echo " Message: " . $sync_response['data']['message'] . "\n";
|
||||
}
|
||||
if (isset($sync_response['data']['post_types_count'])) {
|
||||
echo " Post Types Synced: " . $sync_response['data']['post_types_count'] . "\n";
|
||||
}
|
||||
if (isset($sync_response['data']['taxonomies_count'])) {
|
||||
echo " Taxonomies Synced: " . $sync_response['data']['taxonomies_count'] . "\n";
|
||||
}
|
||||
} else {
|
||||
echo "❌ Structure sync failed\n";
|
||||
if (isset($sync_response['error'])) {
|
||||
echo " Error: " . $sync_response['error'] . "\n";
|
||||
}
|
||||
if (isset($sync_response['raw_error'])) {
|
||||
echo " Details: " . json_encode($sync_response['raw_error']) . "\n";
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
|
||||
// Test 6: Verify backend stored the data
|
||||
echo "Test 6: Verifying Backend Stored Data...\n";
|
||||
|
||||
$verify_response = $api->get('/v1/integration/integrations/' . $integration['id'] . '/content-types/');
|
||||
|
||||
if ($verify_response['success'] && isset($verify_response['data'])) {
|
||||
$data = $verify_response['data'];
|
||||
echo " Post Types in Backend: " . count($data['post_types'] ?? []) . "\n";
|
||||
echo " Taxonomies in Backend: " . count($data['taxonomies'] ?? []) . "\n";
|
||||
echo " Last Structure Fetch: " . ($data['last_structure_fetch'] ?? 'Unknown') . "\n";
|
||||
echo "✅ Backend data verified\n";
|
||||
} else {
|
||||
echo "⚠️ Could not verify backend data\n";
|
||||
if (isset($verify_response['error'])) {
|
||||
echo " Error: " . $verify_response['error'] . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
echo "=== Test Complete ===\n";
|
||||
echo "✅ All tests passed! Site structure sync is working.\n\n";
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Uninstall Handler
|
||||
*
|
||||
* Cleans up plugin data on uninstall
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// If uninstall not called from WordPress, then exit
|
||||
if (!defined('WP_UNINSTALL_PLUGIN')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Remove all options
|
||||
delete_option('igny8_access_token');
|
||||
delete_option('igny8_refresh_token');
|
||||
delete_option('igny8_email');
|
||||
delete_option('igny8_site_id');
|
||||
delete_option('igny8_last_site_sync');
|
||||
delete_option('igny8_last_site_import_id');
|
||||
delete_option('igny8_token_refreshed_at');
|
||||
delete_option('igny8_bridge_version');
|
||||
|
||||
// Remove all post meta (optional - uncomment if you want to remove all meta on uninstall)
|
||||
/*
|
||||
global $wpdb;
|
||||
$wpdb->query("
|
||||
DELETE FROM {$wpdb->postmeta}
|
||||
WHERE meta_key LIKE '_igny8_%'
|
||||
");
|
||||
*/
|
||||
|
||||
// Unschedule cron jobs
|
||||
$timestamp = wp_next_scheduled('igny8_sync_post_statuses');
|
||||
if ($timestamp) {
|
||||
wp_unschedule_event($timestamp, 'igny8_sync_post_statuses');
|
||||
}
|
||||
|
||||
$timestamp = wp_next_scheduled('igny8_sync_site_data');
|
||||
if ($timestamp) {
|
||||
wp_unschedule_event($timestamp, 'igny8_sync_site_data');
|
||||
}
|
||||
|
||||
$timestamp = wp_next_scheduled('igny8_sync_from_igny8');
|
||||
if ($timestamp) {
|
||||
wp_unschedule_event($timestamp, 'igny8_sync_from_igny8');
|
||||
}
|
||||
|
||||
// Note: Taxonomies and terms are NOT deleted
|
||||
// They remain in WordPress for user reference
|
||||
// Only the taxonomy registration is removed
|
||||
|
||||
Reference in New Issue
Block a user