PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.2
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.2
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
metasync / admin / js / metasync-admin.js

metasync-admin.js in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.7.2, at admin/js/metasync-admin.js

3,615 lines 123.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 (function ($) {
2 'use strict';
3
4 /**
5 * All of the code for your admin-facing JavaScript source
6 * should reside in this file.
7 *
8 * Note: It has been assumed you will write jQuery code here, so the
9 * $ function reference has been prepared for usage within the scope
10 * of this function.
11 *
12 * This enables you to define handlers, for when the DOM is ready:
13 *
14 * $(function() {
15 *
16 * });
17 *
18 * When the window is loaded:
19 *
20 * $( window ).load(function() {
21 *
22 * });
23 *
24 * ...and/or other possibilities.
25 *
26 * Ideally, it is not considered best practise to attach more than a
27 * single DOM-ready or window-load handler for a particular page.
28 * Although scripts in the WordPress core, Plugins and Themes may be
29 * practising this, we should strive to set a better example in our own work.
30 */
31
32 // ========================================
33 // UTILITY FUNCTIONS (Consolidated to reduce duplication)
34 // ========================================
35
36 /**
37 * Get plugin name from config or use default
38 * @returns {string} Plugin name
39 */
40 function getPluginName() {
41 return window.MetasyncConfig && window.MetasyncConfig.pluginName ? window.MetasyncConfig.pluginName : 'Search Atlas';
42 }
43
44 /**
45 * Get OTTO name from config or use default
46 * @returns {string} OTTO name
47 */
48 function getOttoName() {
49 return window.MetasyncConfig && window.MetasyncConfig.ottoName ? window.MetasyncConfig.ottoName : 'OTTO';
50 }
51
52 /**
53 * Whether a Search Atlas API key is currently configured.
54 *
55 * The key is no longer rendered into an editable input — it lives encrypted
56 * server-side and is shown only as a masked, read-only display. Presence is
57 * signalled by the data-has-key attribute on that display element, so the
58 * cleartext key never needs to exist in the DOM for the UI to know it is set.
59 *
60 * @returns {boolean}
61 */
62 function searchAtlasKeyConfigured() {
63 var $display = $('#searchatlas-api-key-display');
64 if ($display.length === 0) {
65 return false;
66 }
67 return $display.attr('data-has-key') === '1';
68 }
69
70 /**
71 * Update the read-only masked API key display.
72 *
73 * @param {string} maskedKey Server-provided masked value (e.g. "••••••••ab12"),
74 * or falsy to render the "Not configured" state.
75 */
76 function setSearchAtlasMaskedDisplay(maskedKey) {
77 var $display = $('#searchatlas-api-key-display');
78 if ($display.length === 0) {
79 return;
80 }
81 if (maskedKey) {
82 $display.text(maskedKey).attr('data-has-key', '1');
83 } else {
84 $display.html('<em style="color:#646970;">Not configured</em>').attr('data-has-key', '0');
85 }
86 }
87
88 /**
89 * Update integration status indicator in header
90 * @param {boolean} isIntegrated - Whether the integration is active
91 * @param {string} statusText - Status text to display
92 * @param {string} titleText - Tooltip text
93 */
94 function updateHeaderStatus(isIntegrated, statusText, titleText) {
95 var $statusIndicator = $('.metasync-integration-status');
96 if ($statusIndicator.length > 0) {
97 $statusIndicator.removeClass('integrated not-integrated warning');
98 if (statusText === 'Warning') {
99 $statusIndicator.addClass('warning');
100 } else if (isIntegrated) {
101 $statusIndicator.addClass('integrated');
102 } else {
103 $statusIndicator.addClass('not-integrated');
104 }
105 $statusIndicator.find('.status-text').text(statusText);
106 $statusIndicator.attr('title', titleText);
107 console.log('🔄 Updated header status to: ' + statusText);
108 }
109
110 // Keep compact header badge in sync with integration status
111 var $badge = $('.metasync-status');
112 if ($badge.length > 0) {
113 $badge.removeClass('connected disconnected warning');
114 if (isIntegrated) {
115 $badge.addClass('connected');
116 $badge.find('.status-text').text('Connected');
117 } else if (statusText === 'Warning') {
118 $badge.addClass('warning');
119 $badge.find('.status-text').text('Warning');
120 } else {
121 $badge.addClass('disconnected');
122 $badge.find('.status-text').text('Not Connected');
123 }
124 }
125
126 // Immediately sync admin bar toolbar status
127 var $adminBarContainer = $('#wp-admin-bar-searchatlas-status');
128 var $adminBarItem = $adminBarContainer.find('.ab-item');
129 if ($adminBarItem.length > 0) {
130 var allClasses = 'searchatlas-synced searchatlas-not-synced searchatlas-warning';
131 var pluginName = (window.MetasyncConfig && window.MetasyncConfig.pluginName) || 'SearchAtlas';
132
133 var targetEmoji, targetSvgCode, barClass, barTitle;
134 if (isIntegrated) {
135 targetEmoji = '\uD83D\uDFE2'; targetSvgCode = '1f7e2';
136 barClass = 'searchatlas-synced';
137 barTitle = pluginName + ' - Synced';
138 } else if (statusText === 'Warning') {
139 targetEmoji = '\uD83D\uDFE1'; targetSvgCode = '1f7e1';
140 barClass = 'searchatlas-warning';
141 barTitle = pluginName + ' - ' + titleText;
142 } else {
143 targetEmoji = '\uD83D\uDD34'; targetSvgCode = '1f534';
144 barClass = 'searchatlas-not-synced';
145 barTitle = pluginName + ' - ' + (titleText || 'Not Synced');
146 }
147
148 // Update emoji (SVG image or text fallback)
149 var emojiImg = $adminBarItem.find('img.emoji');
150 if (emojiImg.length > 0) {
151 emojiImg.attr('alt', targetEmoji);
152 emojiImg.attr('src', emojiImg.attr('src').replace(/1f7e2\.svg|1f534\.svg|1f7e1\.svg/, targetSvgCode + '.svg'));
153 } else {
154 var newHtml = $adminBarItem.html().replace(/\uD83D\uDFE2|\uD83D\uDD34|\uD83D\uDFE1/, targetEmoji);
155 if (!newHtml.includes(targetEmoji) && newHtml.includes(pluginName)) {
156 newHtml = newHtml.replace(pluginName, pluginName + ' ' + targetEmoji);
157 }
158 $adminBarItem.html(newHtml);
159 }
160
161 $adminBarContainer.removeClass(allClasses).addClass(barClass);
162 $adminBarContainer.attr('title', barTitle);
163 $adminBarItem.attr('title', barTitle);
164 }
165 }
166
167 /**
168 * Collect whitelabel form fields for submission
169 * @returns {string} Serialized whitelabel field data
170 */
171 function collectWhitelabelFields() {
172 var whitelabelFields = [];
173
174 // Text/URL fields
175 var logoField = $('input[name="metasync_options[whitelabel][logo]"]');
176 var domainField = $('input[name="metasync_options[whitelabel][domain]"]');
177 var passwordField = $('input[name="metasync_options[whitelabel][settings_password]"]');
178
179 if (logoField.length > 0 && logoField.val()) {
180 whitelabelFields.push('metasync_options[whitelabel][logo]=' + encodeURIComponent(logoField.val()));
181 }
182 if (domainField.length > 0 && domainField.val()) {
183 whitelabelFields.push('metasync_options[whitelabel][domain]=' + encodeURIComponent(domainField.val()));
184 }
185 if (passwordField.length > 0 && passwordField.val()) {
186 whitelabelFields.push('metasync_options[whitelabel][settings_password]=' + encodeURIComponent(passwordField.val()));
187 }
188
189 // Checkbox fields (handle both checked and unchecked)
190 var hideFields = ['hide_dashboard', 'hide_settings', 'hide_indexation_control',
191 'hide_redirections', 'hide_robots', 'hide_sync_log',
192 'hide_compatibility', 'hide_advanced'];
193
194 hideFields.forEach(function (fieldName) {
195 var checkbox = $('input[name="metasync_options[whitelabel][' + fieldName + ']"]');
196 if (checkbox.length > 0) {
197 var value = checkbox.is(':checked') ? '1' : '0';
198 whitelabelFields.push('metasync_options[whitelabel][' + fieldName + ']=' + value);
199 }
200 });
201
202 return whitelabelFields.join('&');
203 }
204
205 /**
206 * White-label branding fields that are mirrored into the metasync.php
207 * plugin header. Keyed by the form input name.
208 */
209 var brandingFieldConfigs = {
210 'metasync_options[general][white_label_plugin_name]': { label: 'Plugin Name', url: false },
211 'metasync_options[general][white_label_plugin_description]': { label: 'Plugin Description', url: false },
212 'metasync_options[general][white_label_plugin_author]': { label: 'Author', url: false },
213 'metasync_options[general][white_label_plugin_author_uri]': { label: 'Author URL', url: true },
214 'metasync_options[general][white_label_plugin_uri]': { label: 'Plugin URL', url: true }
215 };
216
217 /**
218 * Client-side mirror of the server-side plugin-header contract for the
219 * white-label branding fields, for immediate feedback while typing or
220 * submitting. The server-side validation stays authoritative.
221 *
222 * @param {string} value Trimmed field value.
223 * @param {boolean} isUrl Whether the value must be an http(s) URL.
224 * @returns {string} Empty when valid, otherwise the problem description.
225 */
226 function validateBrandingValue(value, isUrl) {
227 if (value === '') {
228 return '';
229 }
230 if (/[\r\n]/.test(value)) {
231 return 'must be a single line';
232 }
233 if (value.indexOf('*/') !== -1) {
234 return 'must not contain "*/"';
235 }
236 if (/<\?php|\?>/i.test(value)) {
237 return 'must not contain PHP tags';
238 }
239 // Control characters, line/paragraph separators, and invisible format
240 // characters (including the bidirectional marks used for brand spoofing).
241 // Unicode property escapes cover exactly the ranges the server checks
242 // (Cc, Cf, Zl, Zp) without literal control-character sequences.
243 try {
244 if (/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(value)) {
245 return 'must not contain control or invisible characters';
246 }
247 } catch (e) {
248 // Very old engines without Unicode property escapes: fall through,
249 // the server-side check remains authoritative.
250 }
251 // Mirror of the server allow-list: letters (incl. Unicode), marks,
252 // numbers, punctuation, symbols and space separators (incl. NBSP and
253 // the ideographic space, which the server-side contract accepts).
254 try {
255 if (!/^[\p{L}\p{M}\p{N}\p{P}\p{S}\p{Zs}]+$/u.test(value)) {
256 return 'contains characters that are not allowed';
257 }
258 } catch (e) {
259 // Very old engines without Unicode property escapes: fall through,
260 // the server-side check remains authoritative.
261 }
262 if (isUrl) {
263 var parsed;
264 try {
265 parsed = new URL(value);
266 } catch (e) {
267 return 'must be a valid http:// or https:// address';
268 }
269 if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
270 return 'must be a valid http:// or https:// address';
271 }
272 }
273 return '';
274 }
275
276 /**
277 * Remove all inline white-label branding field errors.
278 */
279 function clearBrandingFieldErrors() {
280 $('.metasync-branding-field-error').remove();
281 }
282
283 /**
284 * Show an inline error beside a branding input. Accepts either the full
285 * input name or the bare setting key (as returned by the server).
286 *
287 * @param {string} fieldKey Input name or white-label setting key.
288 * @param {string} message Error message to display.
289 */
290 function showBrandingFieldError(fieldKey, message) {
291 var inputName = brandingFieldConfigs[fieldKey]
292 ? fieldKey
293 : 'metasync_options[general][' + fieldKey + ']';
294 var input = $('input[name="' + inputName + '"]');
295 if (input.length === 0) {
296 return;
297 }
298 input.last().after(
299 $('<p>').addClass('description metasync-branding-field-error')
300 .css({ color: 'var(--dashboard-error, #b32d2e)', fontWeight: 600, marginTop: '4px' })
301 .text(message)
302 );
303 }
304
305 /**
306 * Validate every branding input currently in the DOM (both save paths —
307 * the form submit handler and the floating "Save Now" button — call this).
308 * Renders the inline per-field errors and returns the message list.
309 *
310 * @returns {string[]} Error messages; empty when everything is valid.
311 */
312 function validateBrandingFieldsInDom() {
313 clearBrandingFieldErrors();
314 var problems = [];
315 Object.keys(brandingFieldConfigs).forEach(function (inputName) {
316 var config = brandingFieldConfigs[inputName];
317 var input = $('input[name="' + inputName + '"]');
318 if (input.length === 0) {
319 return;
320 }
321 var problem = validateBrandingValue(String(input.val() || '').trim(), config.url);
322 if (problem) {
323 var fullMessage = config.label + ' ' + problem + '.';
324 showBrandingFieldError(inputName, fullMessage);
325 problems.push(fullMessage);
326 }
327 });
328 return problems;
329 }
330
331 /**
332 * Display notice message in plugin area
333 * @param {string} type - Notice type: 'success' or 'error'
334 * @param {string} title - Notice title
335 * @param {string} message - Notice message
336 * @param {string} cssClass - Additional CSS class for the notice
337 * @param {number} autoHideDelay - Auto-hide delay in ms (0 = no auto-hide)
338 */
339 function showPluginNotice(type, title, message, cssClass, autoHideDelay) {
340 cssClass = cssClass || 'metasync-notice';
341 autoHideDelay = autoHideDelay || 0;
342
343 var noticeClass = type === 'success' ? 'notice-success' : 'notice-error';
344 var noticeHTML = '<div class="notice ' + noticeClass + ' is-dismissible ' + cssClass + '" style="margin: 20px 0; padding: 12px;">' +
345 '<p><strong>' + title + '</strong><br/>' + message + '</p>' +
346 '</div>';
347
348 // Remove existing notices of same class
349 $('.' + cssClass).remove();
350
351 // Insert between navigation menu and page content
352 var $navWrapper = $('.metasync-nav-wrapper');
353 if ($navWrapper.length > 0) {
354 $navWrapper.after(noticeHTML);
355 } else {
356 $('.metasync-dashboard-wrap').prepend(noticeHTML);
357 }
358
359 // Scroll to the top to ensure visibility
360 $('html, body').animate({ scrollTop: 0 }, 'slow');
361
362 // Auto-hide if delay specified
363 if (autoHideDelay > 0) {
364 setTimeout(function () {
365 $('.' + cssClass).fadeOut(300, function () {
366 $(this).remove();
367 });
368 }, autoHideDelay);
369 }
370 }
371
372 /**
373 * Prevent dashboard.js interference with button
374 * @param {jQuery} $button - Button element to protect
375 */
376 function preventDashboardInterference($button) {
377 $button.removeClass('dashboard-loading');
378 $button.addClass('no-loading metasync-sa-connect-protected');
379 $button.prop('disabled', false);
380 }
381
382 // ========================================
383 // ORIGINAL FUNCTIONS
384 // ========================================
385
386 function metasync_syncPostsAndPages() {
387 wp.ajax.post('metasync_send_customer_params', {})
388 .done(function (response) {
389 console.log(response);
390 });
391 }
392
393 function metasyncGenerateAPIKey() {
394 return Math.random().toString(36).substring(2, 15) +
395 Math.random().toString(36).substring(2, 15);
396 }
397
398 function metasyncLGLogin(user, pass) {
399 jQuery.post(ajaxurl, {
400 action: 'metasync_lglogin',
401 username: user, password: pass,
402 nonce: metaSync.lglogin_nonce || ''
403 }, function (response) {
404 if (typeof response.token !== 'undefined') {
405 $('#linkgraph_token').val(response.token);
406 $('#linkgraph_customer_id').val(response.customer_id);
407 $('.input.lguser,#lgerror').addClass('hidden');
408 localStorage.setItem('token', response.token);
409 } else {
410 $('#lgerror').text(response.detail + ' (' + response.kind + ')').removeClass('hidden');
411 }
412 }
413 );
414 }
415
416 function setToken() {
417 if ($('#linkgraph_token') && $('#linkgraph_token').val()) {
418 localStorage.setItem('token', $('#linkgraph_token').val());
419 }
420 }
421
422 // Search Atlas Connect functions
423 // Handles 1-click authentication to retrieve Search Atlas API key and Otto UUID.
424 // Does NOT create WordPress login sessions.
425 var saConnectPollingInterval = null;
426 var saConnectWindow = null;
427
428 function handleSearchAtlasConnect() {
429 var $button = $('#connect-searchatlas-btn');
430 // Only check for button (status/progress elements are created dynamically)
431 if (!$button.length) {
432 return;
433 }
434
435 // Enhanced loading state with spinner and CSS class (prevent dashboard.js conflicts)
436 $button.prop('disabled', true)
437 .addClass('connecting no-loading') // Add 'no-loading' to prevent dashboard.js interference
438 .removeClass('dashboard-loading') // Remove any existing dashboard loading
439 .html('<span class="metasync-sa-connect-loading"></span> Initializing...');
440
441 // Hide any existing status/progress containers (may not exist yet)
442 $('#sa-connect-status-message').hide();
443 $('.metasync-sa-connect-progress').hide();
444
445 // Initialize progress display immediately (no separate status message)
446 initializeProgressDisplay();
447
448 // Generate nonce for WordPress AJAX security
449 var ajaxNonce = metaSync.sa_connect_nonce || '';
450 if (!ajaxNonce) {
451 return;
452 }
453
454 // Make AJAX call to generate SSO URL
455 var ajaxUrl = ajaxurl || metaSync.ajax_url;
456
457
458 $.ajax({
459 url: ajaxUrl,
460 type: 'POST',
461 data: {
462 action: 'metasync_generate_connect_url',
463 nonce: ajaxNonce
464 },
465 timeout: 30000, // 30 second timeout
466 success: function (response) {
467
468
469 if (response.success) {
470
471 // Update button state
472 $button.removeClass('connecting dashboard-loading')
473 .addClass('authenticating no-loading') // Maintain no-loading class
474 .html('<span class="metasync-sa-connect-loading"></span> Opening Authentication...');
475
476
477
478 // Small delay for better UX (let user see the message)
479 setTimeout(function () {
480 console.log('🔍 Opening connect popup with URL:', response.data.connect_url);
481
482 // Detect mobile device for better experience
483 var isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
484 var windowFeatures;
485
486
487
488 if (isMobile) {
489 // On mobile, open in same tab for better experience
490 showConnectInfo('📱 Mobile Authentication',
491 'Opening ' + getPluginName() + ' authentication. You\'ll be redirected back after logging in.');
492 window.location.href = response.data.connect_url;
493 return;
494 } else {
495 // Desktop: enhanced popup window
496 var screenWidth = window.screen.width;
497 var screenHeight = window.screen.height;
498 var windowWidth = Math.min(650, screenWidth * 0.8);
499 var windowHeight = Math.min(750, screenHeight * 0.8);
500 var left = (screenWidth - windowWidth) / 2;
501 var top = (screenHeight - windowHeight) / 2;
502
503 windowFeatures = 'width=' + windowWidth +
504 ',height=' + windowHeight +
505 ',left=' + left +
506 ',top=' + top +
507 ',scrollbars=yes,resizable=yes,toolbar=no,location=yes,status=yes';
508
509
510 }
511
512
513
514 // Open SSO URL in popup with enhanced window properties
515 saConnectWindow = window.open(
516 response.data.connect_url,
517 'searchatlas-connect',
518 windowFeatures
519 );
520
521
522
523 // Enhanced popup blocked detection
524 setTimeout(function () {
525 if (!saConnectWindow || saConnectWindow.closed || typeof saConnectWindow.closed === 'undefined') {
526 showConnectError('🚫 Popup Blocked',
527 'Your browser blocked the authentication popup. Please allow popups for this site and try again.',
528 [{
529 text: '🔄 Try Again',
530 action: function () {
531 handleSearchAtlasConnect();
532 }
533 }, {
534 text: '📝 How to Enable Popups',
535 action: function () {
536 showPopupHelp();
537 }
538 }, {
539 text: '🖥️ Open in New Tab',
540 action: function () {
541 window.open(response.data.connect_url, '_blank');
542 startSearchAtlasPolling(response.data.nonce_token);
543 }
544 }]
545 );
546 resetConnectButton();
547 return;
548 }
549
550 // Add focus to popup window
551 try {
552 saConnectWindow.focus();
553 } catch(e) {
554 // Ignore focus errors
555 }
556
557 // Update progress display and start polling
558 updateProgress(10, 1, 6); // Show initial progress
559 console.log('🔍 Starting connect polling...');
560 setTimeout(function () {
561 startSearchAtlasPolling(response.data.nonce_token);
562 }, 200);
563
564 }, 100); // Small delay to let popup settle
565
566 }, 500); // 500ms delay for better UX
567
568 } else {
569 var errorMessage = response.data.message || 'Failed to generate connect URL';
570 showConnectError('❌ Connection Failed',
571 errorMessage,
572 [{
573 text: '🔄 Retry Connection',
574 action: function () {
575 handleSearchAtlasConnect();
576 }
577 }]
578 );
579 resetConnectButton();
580 }
581 },
582 error: function (xhr, status, error) {
583 console.error('🐛 DEBUG: AJAX error occurred:', {
584 xhr: xhr,
585 status: status,
586 error: error,
587 responseText: xhr.responseText,
588 responseJSON: xhr.responseJSON,
589 readyState: xhr.readyState,
590 ajaxUrl: ajaxUrl
591 });
592
593 var errorMessage = 'Network error occurred while connecting to ' + getPluginName();
594
595 // Provide specific error messages based on the error type
596 if (status === 'timeout') {
597 errorMessage = 'Request timed out. Please check your internet connection and try again.';
598 } else if (status === 'error') {
599 if (xhr.status === 0) {
600 errorMessage = 'Unable to connect. Please check if WordPress admin-ajax.php is accessible.';
601 } else if (xhr.status === 403) {
602 errorMessage = 'Access denied. Please refresh the page and try again.';
603 } else if (xhr.status === 500) {
604 errorMessage = 'Server error occurred. Please check server logs for details.';
605 } else {
606 errorMessage = 'HTTP Error ' + xhr.status + ': ' + xhr.statusText;
607 }
608 } else if (xhr.responseJSON && xhr.responseJSON.message) {
609 errorMessage = xhr.responseJSON.message;
610 }
611
612 showConnectError('🌐 Network Error', errorMessage,
613 [{
614 text: '🔄 Retry Connection',
615 action: function () {
616 handleSearchAtlasConnect();
617 }
618 }, {
619 text: '🔧 Check Network',
620 action: function () {
621 console.log('Network diagnostics:', {
622 status: status,
623 error: error,
624 xhr: xhr
625 });
626 }
627 }]
628 );
629 resetConnectButton();
630 }
631 });
632 }
633
634 function resetConnectButton() {
635 var $button = $('#connect-searchatlas-btn');
636 var $progressContainer = $('.metasync-sa-connect-progress');
637 var hasApiKey = searchAtlasKeyConfigured();
638
639 $button.prop('disabled', false)
640 .removeClass('connecting authenticating success dashboard-loading') // Remove all loading classes
641 .html(hasApiKey ? '🔄 Re-authenticate with ' + getPluginName() : '🔗 Connect to ' + getPluginName());
642 $progressContainer.hide();
643 }
644
645 function startSearchAtlasPolling(nonceToken) {
646 var pollCount = 0;
647 // WP-558 (Option A): poll for 2 minutes (24 * 5s). Each poll also drives
648 // the outbound pull-based connect fallback server-side, which needs a
649 // slightly longer window than the legacy push-only 60s to ride out the
650 // human SSO-authorization delay. ~24 requests stays inside the CA
651 // endpoint's 120/hour per-domain budget.
652 var maxPolls = 24; // Poll for 120 seconds (24 * 5 seconds)
653 var $progressContainer = $('.metasync-sa-connect-progress');
654 var $progressFill = $('.metasync-sa-connect-progress-fill');
655 var $progressText = $('.metasync-sa-connect-progress-text');
656 var $button = $('#connect-searchatlas-btn');
657
658 // Progress display should already be initialized, just update it
659 updateProgress(0, 0, maxPolls);
660
661
662 saConnectPollingInterval = setInterval(function () {
663 pollCount++;
664
665
666 // Update progress bar
667 var progress = Math.min((pollCount / maxPolls) * 100, 100);
668 updateProgress(progress, pollCount, maxPolls);
669
670 // Check if window was closed manually - but continue polling
671 if (saConnectWindow && saConnectWindow.closed) {
672 // Popup closed, but continue polling to check for authentication success
673 console.log('🔍 Connect popup closed, continuing to poll for authentication success...');
674 saConnectWindow = null; // Clear reference to closed window
675
676 // Update UI to show we're still checking
677 updateProgress(75, pollCount, maxPolls);
678 $progressText.text('Popup closed - checking authentication status...');
679
680 // Continue polling - don't return, let the polling continue
681 }
682
683 // Stop polling after max attempts
684 if (pollCount >= maxPolls) {
685 stopSearchAtlasPolling();
686 if (saConnectWindow) {
687 saConnectWindow.close();
688 }
689
690 // �
691 Reset the authentication flow while keeping the timeout component
692 resetConnectButton();
693
694 showConnectError('⏰ Authentication Timeout',
695 'The authentication process timed out after 2 minutes. Please complete the authentication more quickly or check for network issues.',
696 [{
697 text: '🔄 Try Again',
698 action: function () {
699 handleSearchAtlasConnect();
700 }
701 }, {
702 text: '💬 Contact Support',
703 action: function () {
704 var supportEmail = metaSync.support_email || '[email protected]';
705 window.open('mailto:' + supportEmail + '?subject=Connect Authentication Timeout (120s)', '_blank');
706 }
707 }]
708 );
709 return;
710 }
711
712 // Update button state periodically
713 if (pollCount % 2 === 0) { // Every 10 seconds
714 var timeLeft = Math.ceil((maxPolls - pollCount) * 5);
715 $button.html('<span class="metasync-sa-connect-loading"></span> Waiting for Authentication (' + timeLeft + 's left)');
716 }
717
718 // Check if API key was updated
719 $.ajax({
720 url: ajaxurl,
721 type: 'POST',
722 data: {
723 action: 'metasync_check_connect_status',
724 nonce: metaSync.sa_connect_nonce || '',
725 nonce_token: nonceToken
726 },
727 success: function (response) {
728 if (response.success && response.data.updated) {
729 stopSearchAtlasPolling();
730 if (saConnectWindow) {
731 saConnectWindow.close();
732 }
733
734 // Show completion animation
735 updateProgress(100, maxPolls, maxPolls);
736
737 var statusCode = response.data.status_code || 200;
738
739 // Handle different status codes with enhanced UX
740 if (statusCode === 200) {
741 // Success: Update all UI elements to reflect connected state
742 updateUIForConnectedState(response.data.masked_key, response.data.otto_pixel_uuid);
743
744 // Refresh Plugin Auth Token field to show the auto-generated token
745 // This will either show the existing token or the newly auto-generated one
746 $.ajax({
747 url: ajaxurl,
748 type: 'POST',
749 data: {
750 action: 'metasync_get_plugin_auth_token',
751 nonce: metaSync.sa_connect_nonce || ''
752 },
753 success: function (tokenResponse) {
754 if (tokenResponse.success && tokenResponse.data.plugin_auth_token) {
755 $('#apikey').val(tokenResponse.data.plugin_auth_token);
756 console.log('🔑 Plugin Auth Token field updated after connect success');
757 }
758 },
759 error: function () {
760 console.log('⚠️ Could not refresh Plugin Auth Token field, but connect authentication was successful');
761 }
762 });
763
764 $button.removeClass('connecting authenticating dashboard-loading')
765 .addClass('success no-loading')
766 .html('�
767 Authentication Complete!');
768
769 // Add success animation to container
770 $button.closest('.metasync-sa-connect-container').addClass('metasync-sa-connect-success-animation');
771 setTimeout(function () {
772 $button.closest('.metasync-sa-connect-container').removeClass('metasync-sa-connect-success-animation');
773 }, 600);
774
775 // Track 1-click activation in GA4
776 var hasExistingApiKey = searchAtlasKeyConfigured();
777 $.ajax({
778 url: metaSync.ajax_url,
779 type: 'POST',
780 data: {
781 action: 'metasync_track_one_click_activation',
782 nonce: metaSync.nonce,
783 auth_method: 'searchatlas_connect',
784 is_reconnection: hasExistingApiKey
785 }
786 });
787 if (typeof window.metasyncGA4Track === 'function') {
788 window.metasyncGA4Track('one_click_activation', {
789 auth_method: 'searchatlas_connect',
790 is_reconnection: hasExistingApiKey
791 });
792 }
793
794 showConnectSuccess('🎉 Authentication Successful',
795 'Your ' + getPluginName() + ' account has been synced successfully! The page will reload to apply your new settings.',
796 [{
797 text: '🔄 Reload Now',
798 action: function () {
799 location.reload();
800 }
801 }]
802 );
803
804 // Auto-reload with countdown
805 var countdown = 3;
806 var countdownInterval = setInterval(function () {
807 countdown--;
808 if (countdown > 0) {
809 $button.html('�
810 Reloading in ' + countdown + '...');
811 } else {
812 clearInterval(countdownInterval);
813 location.reload();
814 }
815 }, 1000);
816
817 } else if (statusCode === 404) {
818 // Website not registered
819 var effectiveDomain = response.data.effective_domain || metaSync.dashboard_domain;
820 showConnectNotRegistered(effectiveDomain);
821 resetConnectButton();
822
823 } else if (statusCode === 500) {
824 // Server error
825 showConnectError('🔧 Server Error',
826 'A server error occurred during authentication. This is usually temporary.',
827 [{
828 text: '🔄 Try Again',
829 action: function () {
830 handleSearchAtlasConnect();
831 }
832 }, {
833 text: '💬 Contact Support',
834 action: function () {
835 var supportEmail = metaSync.support_email || '[email protected]';
836 window.open('mailto:' + supportEmail + '?subject=Connect Server Error (Code 500)', '_blank');
837 }
838 }]
839 );
840 resetConnectButton();
841
842 } else {
843 // Unknown status
844 showConnectError('❓ Unexpected Status',
845 'Received an unexpected status code (' + statusCode + ') during authentication.',
846 [{
847 text: '🔄 Try Again',
848 action: function () {
849 handleSearchAtlasConnect();
850 }
851 }]
852 );
853 resetConnectButton();
854 }
855 }
856 },
857 error: function (xhr, status, error) {
858 // Continue polling even if individual request fails, but provide feedback
859 if (pollCount % 6 === 0) { // Every 30 seconds, show a subtle warning
860 console.log('Connect polling request failed, continuing... Error:', error);
861 // Don't show error to user for temporary network issues during polling
862 }
863
864 }
865 });
866 }, 5000); // Poll every 5 seconds
867 }
868
869 function initializeProgressDisplay() {
870 var $progressContainer = $('.metasync-sa-connect-progress');
871 var $button = $('#connect-searchatlas-btn');
872
873 // Hide any existing status messages to avoid duplication
874 hideConnectStatus();
875
876 // Create progress elements if they don't exist
877 if ($progressContainer.length === 0) {
878 var progressHTML = `
879 <div class="metasync-sa-connect-progress">
880 <div class="metasync-sa-connect-progress-header">
881 <strong>🔐 Authentication in Progress</strong>
882 <span class="metasync-sa-connect-progress-time">Connecting...</span>
883 </div>
884 <div class="metasync-sa-connect-progress-bar">
885 <div class="metasync-sa-connect-progress-fill"></div>
886 </div>
887 <div class="metasync-sa-connect-progress-text">
888 Establishing secure connection to ' + getPluginName() + '...
889 </div>
890 </div>
891 `;
892 $button.closest('.metasync-sa-connect-container').append(progressHTML);
893 $progressContainer = $('.metasync-sa-connect-progress');
894 }
895
896 $progressContainer.show().find('.metasync-sa-connect-progress-fill').css('width', '0%');
897 }
898
899 function updateProgress(percentage, currentPoll, maxPolls) {
900 var $progressFill = $('.metasync-sa-connect-progress-fill');
901 var $progressTime = $('.metasync-sa-connect-progress-time');
902 var $progressText = $('.metasync-sa-connect-progress-text');
903
904 // Update progress bar
905 $progressFill.css('width', percentage + '%');
906
907 // Update time display (now in seconds)
908 var timeElapsed = currentPoll * 5;
909 var timeRemaining = (maxPolls - currentPoll) * 5;
910 $progressTime.text(timeElapsed + 's elapsed, ' + timeRemaining + 's remaining');
911
912 // Update progress text based on time elapsed (optimized for 120-second timeout)
913 var progressMessages = [
914 'Establishing connection and opening authentication window...',
915 'Please complete authentication in the popup window...',
916 'Almost done! Finalizing your authentication...'
917 ];
918
919 var messageIndex = Math.floor((currentPoll / maxPolls) * progressMessages.length);
920 messageIndex = Math.min(messageIndex, progressMessages.length - 1);
921 $progressText.text(progressMessages[messageIndex]);
922 }
923
924 // Update the old function name for compatibility
925 function startConnectPolling(nonceToken) {
926 return startSearchAtlasPolling(nonceToken);
927 }
928
929 function stopSearchAtlasPolling() {
930 if (saConnectPollingInterval) {
931 clearInterval(saConnectPollingInterval);
932 saConnectPollingInterval = null;
933 }
934 }
935
936 // Legacy function for backward compatibility
937 function stopConnectPolling() {
938 return stopSearchAtlasPolling();
939 }
940
941 function showConnectSuccess(title, message, actions) {
942 showConnectStatus('success', title, message, actions);
943 }
944
945 function showConnectError(title, message, actions) {
946 showConnectStatus('error', title, message, actions);
947 }
948
949 function showConnectInfo(title, message, actions) {
950 showConnectStatus('info', title, message, actions);
951 }
952
953 function showConnectWarning(title, message, actions) {
954 showConnectStatus('warning', title, message, actions);
955 }
956
957 function showConnectStatus(type, title, message, actions) {
958 var $statusContainer = $('#sa-connect-status-message');
959 var $button = $('#connect-searchatlas-btn');
960
961 // Create enhanced status container if it doesn't exist
962 if ($statusContainer.length === 0 || !$statusContainer.hasClass('metasync-sa-connect-status')) {
963 // Create new enhanced status container
964 var statusHTML = '<div id="sa-connect-status-message" class="metasync-sa-connect-status"></div>';
965 $button.closest('.metasync-sa-connect-container').length === 0 ?
966 $button.parent().append(statusHTML) :
967 $button.closest('.metasync-sa-connect-container').append(statusHTML);
968 $statusContainer = $('#sa-connect-status-message');
969 }
970
971 // Build status content
972 var html = '<div class="metasync-sa-connect-status-content">';
973 html += '<div class="metasync-sa-connect-status-title">' + title + '</div>';
974 if (message) {
975 html += '<div class="metasync-sa-connect-status-message">' + message + '</div>';
976 }
977 html += '</div>';
978
979 // Add action buttons if provided
980 if (actions && actions.length > 0) {
981 html += '<div class="metasync-sa-connect-actions">';
982 actions.forEach(function (action, index) {
983 var buttonClass = action.primary ? 'primary' : 'secondary';
984 html += '<button type="button" class="metasync-sa-connect-btn ' + buttonClass + '" data-action="' + index + '">';
985 html += action.text;
986 html += '</button>';
987 });
988 html += '</div>';
989 }
990
991 // Update status container with animation
992 $statusContainer
993 .removeClass('success error info warning')
994 .addClass(type)
995 .html(html)
996 .hide()
997 .slideDown(300);
998
999 // Bind action handlers
1000 if (actions && actions.length > 0) {
1001 $statusContainer.find('.metasync-sa-connect-btn').off('click').on('click', function () {
1002 var $actionBtn = $(this);
1003 var actionIndex = parseInt($actionBtn.data('action'));
1004 if (actions[actionIndex] && typeof actions[actionIndex].action === 'function') {
1005 var originalText = $actionBtn.text();
1006 $actionBtn.prop('disabled', true)
1007 .addClass('no-loading') // Prevent dashboard.js conflicts
1008 .removeClass('dashboard-loading')
1009 .html('<span class="metasync-sa-connect-loading"></span> ' + originalText);
1010 setTimeout(function () {
1011 actions[actionIndex].action();
1012 }, 100);
1013 }
1014 });
1015 }
1016
1017 // Auto-scroll to status message for better visibility
1018 if (type === 'error' || type === 'warning' || type === 'success') {
1019 setTimeout(function () {
1020 $('html, body').animate({
1021 scrollTop: $statusContainer.offset().top - 100
1022 }, 300);
1023 }, 100);
1024 }
1025 }
1026
1027 function showConnectNotRegistered(dashboardDomain) {
1028 // Use dashboard domain if provided, otherwise fallback to effective domain (includes whitelabel)
1029 var domain = dashboardDomain || metaSync.dashboard_domain;
1030 var registerUrl = domain + '/seo-automation-v3/create-project';
1031
1032 showConnectWarning(
1033 '⚠️ Website Not Registered',
1034 'Your website hasn\'t been registered with ' + getPluginName() + ' yet. Registration is required to enable 1-click connect to retrieve your Search Atlas API key and Otto UUID.',
1035 [{
1036 text: '🌐 Register Website',
1037 action: function () {
1038 try {
1039 var parsedRegister = new URL(registerUrl);
1040 if (parsedRegister.protocol === 'https:' || parsedRegister.protocol === 'http:') {
1041 var a = document.createElement('a');
1042 a.href = parsedRegister.href;
1043 a.target = '_blank';
1044 a.rel = 'noopener';
1045 a.click();
1046 }
1047 } catch (e) { /* invalid URL */ }
1048 },
1049 primary: true
1050 }, {
1051 text: '📚 Learn More About Registration',
1052 action: function () {
1053 var docDomain = metaSync.documentation_domain || 'https://searchatlas.com';
1054 try {
1055 var parsedDoc = new URL(docDomain);
1056 if (parsedDoc.protocol === 'https:' || parsedDoc.protocol === 'http:') {
1057 var a = document.createElement('a');
1058 a.href = parsedDoc.href;
1059 a.target = '_blank';
1060 a.rel = 'noopener';
1061 a.click();
1062 }
1063 } catch (e) { /* invalid URL */ }
1064 }
1065 }, {
1066 text: '🔄 Try Authentication Again',
1067 action: function () {
1068 setTimeout(function () {
1069 handleSearchAtlasConnect();
1070 }, 500);
1071 }
1072 }]
1073 );
1074 }
1075
1076 function hideConnectStatus() {
1077 $('#sa-connect-status-message').slideUp(300);
1078 $('.metasync-sa-connect-progress').slideUp(300);
1079 }
1080
1081 function showPopupHelp() {
1082 var helpContent = `
1083 <div style="max-width: 500px;">
1084 <h3>🔧 How to Enable Popups</h3>
1085 <p><strong>Chrome/Edge:</strong></p>
1086 <ol>
1087 <li>Click the popup blocked icon in the address bar</li>
1088 <li>Select "Always allow popups from this site"</li>
1089 <li>Reload the page and try again</li>
1090 </ol>
1091 <p><strong>Firefox:</strong></p>
1092 <ol>
1093 <li>Click the shield icon in the address bar</li>
1094 <li>Turn off "Block popup windows"</li>
1095 <li>Refresh and try again</li>
1096 </ol>
1097 <p><strong>Safari:</strong></p>
1098 <ol>
1099 <li>Go to Safari → Preferences → Websites</li>
1100 <li>Select "Pop-up Windows" on the left</li>
1101 <li>Set this website to "Allow"</li>
1102 </ol>
1103 </div>
1104 `;
1105
1106 showConnectInfo('📝 Popup Help', helpContent, [{
1107 text: '�
1108 Got it, Try Again',
1109 action: function () {
1110 handleSearchAtlasConnect();
1111 },
1112 primary: true
1113 }]);
1114 }
1115
1116 function enhancedErrorRecovery(error, context) {
1117 console.group('🔍 Connect Error Diagnostics');
1118 console.log('Error Context:', context);
1119 console.log('Error Details:', error);
1120 console.log('Browser Info:', {
1121 userAgent: navigator.userAgent,
1122 cookieEnabled: navigator.cookieEnabled,
1123 language: navigator.language,
1124 platform: navigator.platform
1125 });
1126 console.log('Current Time:', new Date().toISOString());
1127 console.groupEnd();
1128
1129 // Provide contextual recovery suggestions
1130 var recoverySuggestions = [];
1131
1132 if (context === 'network') {
1133 recoverySuggestions = [
1134 'Check your internet connection',
1135 'Disable VPN or proxy if enabled',
1136 'Try refreshing the page',
1137 'Clear browser cache and cookies'
1138 ];
1139 } else if (context === 'popup') {
1140 recoverySuggestions = [
1141 'Allow popups for this website',
1142 'Disable ad blockers temporarily',
1143 'Try using a different browser',
1144 'Check if firewall is blocking the request'
1145 ];
1146 } else if (context === 'timeout') {
1147 recoverySuggestions = [
1148 'Complete authentication within 2 minutes',
1149 'Check if the popup window needs attention',
1150 'Ensure you have your ' + getPluginName() + ' login ready',
1151 'Try the authentication process again',
1152 'Contact support if timeouts persist'
1153 ];
1154 }
1155
1156 return recoverySuggestions;
1157 }
1158
1159 // Add enhanced page visibility handling
1160 function handlePageVisibilityChange() {
1161 if (document.hidden && saConnectWindow && !saConnectWindow.closed) {
1162 // Page became hidden while SSO is in progress
1163 showConnectInfo('👁️ Page Hidden',
1164 'This page is now in the background. The authentication will continue, but you may want to return to this tab to see the results.');
1165 }
1166 }
1167
1168 // Initialize enhanced features when document is ready
1169 $(document).ready(function () {
1170
1171 // Hide Jetpack identity crisis container on plugin pages
1172 if ($('.metasync-dashboard-wrap').length > 0) {
1173 $('#jp-identity-crisis-container, .jp-identity-crisis-container').hide();
1174 }
1175
1176 // Check for URL parameters and show success/error messages
1177 const urlParams = new URLSearchParams(window.location.search);
1178
1179 // Show success message for cleared logs
1180 if (urlParams.get('log_cleared') === '1') {
1181 showSyncSuccess('🧹 Error Logs Cleared', 'All error log entries have been successfully cleared.');
1182 }
1183
1184 // Show success message for cleared error summary
1185 if (urlParams.get('error_summary_cleared') === '1') {
1186 showSyncSuccess('📊 Error Summary Cleared', 'Error summary has been cleared successfully.');
1187 }
1188
1189 // Show error message for failed clear operation
1190 if (urlParams.get('clear_error') === '1') {
1191 showSyncError('❌ Clear Failed', 'Unable to clear the error logs. Please check permissions or try again.');
1192 }
1193
1194 // Show error message for failed error summary clear
1195 if (urlParams.get('error_summary_error') === '1') {
1196 showSyncError('❌ Clear Failed', 'Unable to clear the error summary. Please try again.');
1197 }
1198
1199 // Check global variables are available
1200
1201 // Test AJAX connectivity using our specific endpoint
1202 if (typeof ajaxurl !== 'undefined' && ajaxurl) {
1203 $.ajax({
1204 url: ajaxurl,
1205 type: 'POST',
1206 data: {
1207 action: 'metasync_test_ajax_endpoint',
1208 nonce: metaSync.sa_connect_nonce
1209 },
1210 timeout: 10000,
1211 success: function (response) {
1212 if (!response.success) {
1213 console.warn('🐛 DEBUG: AJAX endpoint reached but returned success=false:', response.data);
1214 }
1215 },
1216 error: function (xhr, status, error) {
1217 console.error('🐛 DEBUG: AJAX test failed:', {
1218 xhr: xhr,
1219 status: status,
1220 error: error,
1221 responseText: xhr.responseText,
1222 ajaxurl: ajaxurl
1223 });
1224
1225 // Try alternative AJAX test
1226 $.post(ajaxurl, {
1227 action: 'wp_ajax_nopriv_heartbeat'
1228 }).done(function (response2) {
1229 }).fail(function (xhr2) {
1230 console.error('🐛 DEBUG: Alternative AJAX also failed:', xhr2);
1231 });
1232 }
1233 });
1234 }
1235
1236 // Check if SSO button exists and is functional
1237 var $connectButton = $('#connect-searchatlas-btn');
1238
1239 // Test direct click event binding and fix dashboard interference
1240 if ($connectButton.length > 0) {
1241 // Aggressively prevent dashboard loading interference
1242 preventDashboardInterference($connectButton);
1243
1244 $connectButton.off('click').on('click', function (e) {
1245
1246 // Prevent dashboard.js from interfering
1247 preventDashboardInterference($(this));
1248
1249 // Call handleSearchAtlasConnect if not already disabled by another process
1250 if (!$(this).hasClass('connecting') && !$(this).hasClass('authenticating')) {
1251 handleSearchAtlasConnect();
1252 } else {
1253 }
1254 });
1255 }
1256
1257 // Add page visibility change handler
1258 if (typeof document.hidden !== 'undefined') {
1259 document.addEventListener('visibilitychange', handlePageVisibilityChange);
1260 }
1261
1262 // Add keyboard shortcuts for better accessibility
1263 $(document).on('keydown', function (e) {
1264 // Escape key to cancel ongoing SSO process
1265 if (e.key === 'Escape' && saConnectPollingInterval) {
1266 if (confirm('Cancel the ongoing authentication process?')) {
1267 stopSearchAtlasPolling();
1268 if (saConnectWindow) {
1269 saConnectWindow.close();
1270 }
1271 showConnectInfo('⏸️ Authentication Cancelled', 'You cancelled the authentication process.');
1272 resetConnectButton();
1273 }
1274 }
1275 });
1276
1277 // Add connection status indicator
1278 function updateConnectionStatus() {
1279 var $button = $('#connect-searchatlas-btn');
1280 var $apiKeyField = $('#searchatlas-api-key-display');
1281
1282 // Only update status if we're on a page with the API key display (General Settings)
1283 // On other pages, preserve the PHP-determined status in the header
1284 if ($apiKeyField.length === 0) {
1285 return; // Don't update status on pages without the API key display
1286 }
1287
1288 var hasApiKey = searchAtlasKeyConfigured();
1289 var hasOttoUuid = metaSync.otto_pixel_uuid && metaSync.otto_pixel_uuid.trim() !== '';
1290 var isFullyConnected = hasApiKey && hasOttoUuid;
1291
1292 // Update button text based on connection state
1293 if (!$button.prop('disabled')) {
1294 if (isFullyConnected) {
1295 $button.html('🔄 Re-authenticate with ' + getPluginName());
1296 } else if (hasApiKey && !hasOttoUuid) {
1297 $button.html('🔧 Complete Authentication Setup');
1298 } else {
1299 $button.html('🔗 Connect to ' + getPluginName());
1300 }
1301 }
1302
1303 // Update header status indicator (only on General Settings page).
1304 // the header badge must reflect the confirmed heartbeat
1305 // round-trip (metaSync.is_connected), NOT mere API-key/UUID presence —
1306 // otherwise a revoked/invalid key still shows "Connected" because the
1307 // key string and UUID remain stored locally.
1308 // NOTE: wp_localize_script stringifies PHP booleans (true -> "1",
1309 // false -> ""), so normalise to a real boolean rather than ===.
1310 var heartbeatConnected = metaSync.is_connected === true
1311 || metaSync.is_connected === '1'
1312 || metaSync.is_connected === 1;
1313 if (isFullyConnected && heartbeatConnected) {
1314 updateHeaderStatus(true, 'Synced', getPluginName() + ' API key and ' + getOttoName() + ' UUID are configured');
1315 } else if (isFullyConnected) {
1316 // Key + UUID are configured but the heartbeat is not succeeding
1317 // (e.g. the key was revoked on the Search Atlas side).
1318 updateHeaderStatus(false, 'Not Connected', getPluginName() + ' API key is set but the heartbeat is not responding — the connection may be revoked or unreachable.');
1319 } else if (hasApiKey && !hasOttoUuid) {
1320 updateHeaderStatus(false, 'Warning', 'Connected but ' + getOttoName() + ' UUID is missing — deploys will not work. Please reconnect.');
1321 } else {
1322 updateHeaderStatus(false, 'Not Synced', 'Missing ' + getPluginName() + ' API key or ' + getOttoName() + ' UUID');
1323 }
1324 }
1325
1326 // The API key is no longer manually editable (one-click auth only), so there
1327 // is no input event to monitor; status changes are driven by connect/disconnect.
1328
1329 // Initial status update
1330 updateConnectionStatus();
1331
1332 // Initialize dashboard iframe functionality
1333 initializeDashboardIframe();
1334
1335 // Settings dropdown now handled by inline script in HTML
1336
1337 // Add debug function for connection status (accessible in console)
1338 window.debugConnectionStatus = function () {
1339 var hasApiKey = searchAtlasKeyConfigured();
1340
1341 console.log('🔍 Connection Status Debug:', {
1342 searchatlas_credential_state: hasApiKey ? 'CONFIGURED (masked)' : 'EMPTY',
1343 otto_pixel_uuid: metaSync.otto_pixel_uuid || 'NOT SET',
1344 connection_state: hasApiKey && metaSync.otto_pixel_uuid ? 'CONNECTED' :
1345 hasApiKey ? 'PARTIAL (Missing ' + getOttoName() + ' UUID)' : 'NOT CONNECTED',
1346 dashboard_tab_visible: hasApiKey && metaSync.otto_pixel_uuid ? 'YES' : 'NO',
1347 status_indicator_should_show: hasApiKey && metaSync.otto_pixel_uuid ? 'Synced' : 'Not Synced'
1348 });
1349 };
1350
1351
1352 });
1353
1354 // Settings dropdown is now handled by inline script in HTML for better reliability
1355
1356 /**
1357 * Initialize Dashboard Iframe functionality
1358 * Adds loading states and error handling for the embedded dashboard
1359 */
1360 function initializeDashboardIframe() {
1361 var $iframe = $('#metasync-dashboard-iframe');
1362
1363 if ($iframe.length === 0) {
1364 return; // No iframe on this page
1365 }
1366
1367 // Add loading indicator
1368 var $wrapper = $('.metasync-dashboard-iframe-wrapper');
1369 var loadingHTML = '<div class="metasync-dashboard-iframe-loading"><div class="spinner"></div><p>Loading dashboard...</p></div>';
1370 $wrapper.append(loadingHTML);
1371
1372 // Handle iframe load events
1373 $iframe.on('load', function () {
1374 $('.metasync-dashboard-iframe-loading').fadeOut(300);
1375
1376 // Log successful load
1377 console.log('Dashboard iframe loaded successfully');
1378 });
1379
1380 // Handle iframe error events
1381 $iframe.on('error', function () {
1382 $('.metasync-dashboard-iframe-loading').html(
1383 '<div style="text-align: center; color: #dc3232;">' +
1384 '<h3>❌ Dashboard Loading Error</h3>' +
1385 '<p>Unable to load the dashboard. Please check your connection.</p>' +
1386 '<button type="button" class="button button-primary" onclick="location.reload();">🔄 Reload Page</button>' +
1387 '</div>'
1388 );
1389
1390 console.error('Dashboard iframe failed to load');
1391 });
1392
1393 // Add keyboard shortcut for refreshing iframe
1394 $(document).on('keydown', function (e) {
1395 // Ctrl/Cmd + R on dashboard page refreshes iframe
1396 if ((e.ctrlKey || e.metaKey) && e.key === 'r' && $iframe.length > 0) {
1397 e.preventDefault();
1398 refreshDashboardIframe();
1399 }
1400 });
1401
1402 // Handle iframe resize for better mobile experience
1403 function adjustIframeHeight() {
1404 if (window.innerWidth <= 768) {
1405 $iframe.height(600);
1406 } else {
1407 $iframe.height(800);
1408 }
1409 }
1410
1411 // Adjust on window resize
1412 $(window).on('resize', adjustIframeHeight);
1413 adjustIframeHeight(); // Initial adjustment
1414 }
1415
1416 /**
1417 * Refresh Dashboard Iframe
1418 * Reloads the iframe content with loading indicator
1419 */
1420 function refreshDashboardIframe() {
1421 var $iframe = $('#metasync-dashboard-iframe');
1422 var $wrapper = $('.metasync-dashboard-iframe-wrapper');
1423
1424 if ($iframe.length === 0) {
1425 return;
1426 }
1427
1428 // Show loading indicator
1429 $('.metasync-dashboard-iframe-loading').remove();
1430 var loadingHTML = '<div class="metasync-dashboard-iframe-loading"><div class="spinner"></div><p>Refreshing dashboard...</p></div>';
1431 $wrapper.append(loadingHTML);
1432
1433 // Refresh iframe
1434 var currentSrc = $iframe.attr('src');
1435 $iframe.attr('src', '');
1436 setTimeout(function () {
1437 $iframe.attr('src', currentSrc);
1438 }, 100);
1439
1440 console.log('Dashboard iframe refresh initiated');
1441 }
1442
1443 /**
1444 * Handle Search Atlas Authentication Reset.
1445 * Shows confirmation dialog and clears the Search Atlas API key and Otto UUID.
1446 */
1447 function handleSearchAtlasResetAuth() {
1448 // Show confirmation dialog
1449 var confirmed = confirm(
1450 '⚠️ Disconnect ' + getPluginName() + ' Account\n\n' +
1451 'This will:\n' +
1452 '• Remove your ' + getPluginName() + ' API key\n' +
1453 '• Clear all authentication tokens\n' +
1454 '• Reset connection timestamps\n' +
1455 '• Clear cached authentication data\n\n' +
1456 'You will need to re-authenticate to use ' + getPluginName() + ' features.\n\n' +
1457 'Are you sure you want to continue?'
1458 );
1459
1460 if (!confirmed) {
1461 return;
1462 }
1463
1464 var $resetButton = $('#reset-searchatlas-auth');
1465 var $connectButton = $('#connect-searchatlas-btn');
1466
1467 // Show loading state (prevent dashboard.js conflicts)
1468 $resetButton.prop('disabled', true)
1469 .addClass('no-loading') // Prevent dashboard.js interference
1470 .removeClass('dashboard-loading')
1471 .html('<span class="metasync-sa-connect-loading"></span> Disconnecting...');
1472
1473 // Show status message
1474 showConnectInfo('🔄 Disconnecting', 'Clearing your ' + getPluginName() + ' authentication data...');
1475
1476 // Make AJAX call to reset authentication
1477 $.ajax({
1478 url: ajaxurl,
1479 type: 'POST',
1480 data: {
1481 action: 'metasync_reset_authentication',
1482 nonce: metaSync.reset_auth_nonce
1483 },
1484 success: function (response) {
1485 if (response.success) {
1486 // Reset the masked display to the "Not configured" state
1487 setSearchAtlasMaskedDisplay('');
1488
1489 // Update button states
1490 $connectButton.html('🔗 Connect to ' + getPluginName());
1491 $resetButton.remove(); // Remove reset button since no longer connected
1492
1493 // Show clean success message without duplicate connect functionality
1494 showConnectSuccess('�
1495 Account Disconnected',
1496 'Your ' + getPluginName() + ' authentication has been completely reset. All authentication data has been cleared.',
1497 [{
1498 text: '📄 View What Was Cleared',
1499 action: function () {
1500 showClearedDataDetails(response.data.cleared_data);
1501 },
1502 primary: true
1503 }, {
1504 text: '�
1505 Got it',
1506 action: function () {
1507 hideConnectStatus();
1508 }
1509 }]
1510 );
1511
1512 // Update page elements to reflect disconnected state
1513 updateUIForDisconnectedState();
1514
1515 } else {
1516 showConnectError('❌ Reset Failed',
1517 response.data.message || 'Failed to reset authentication',
1518 [{
1519 text: '🔄 Try Again',
1520 action: function () {
1521 handleSearchAtlasResetAuth();
1522 }
1523 }, {
1524 text: '💬 Contact Support',
1525 action: function () {
1526 var supportEmail = metaSync.support_email || '[email protected]';
1527 window.open('mailto:' + supportEmail + '?subject=Authentication Reset Failed', '_blank');
1528 }
1529 }]
1530 );
1531 }
1532 },
1533 error: function (xhr, status, error) {
1534 showConnectError('🌐 Network Error',
1535 'A network error occurred while trying to reset authentication.',
1536 [{
1537 text: '🔄 Try Again',
1538 action: function () {
1539 handleSearchAtlasResetAuth();
1540 }
1541 }]
1542 );
1543 },
1544 complete: function () {
1545 // Reset button state
1546 $resetButton.prop('disabled', false)
1547 .removeClass('dashboard-loading no-loading')
1548 .html('🔓 Disconnect Account');
1549 }
1550 });
1551 }
1552
1553 /**
1554 * Show details of what data was cleared during reset
1555 */
1556 function showClearedDataDetails(clearedData) {
1557 var details = '<div style="max-width: 500px;"><h3>🗑️ Data Cleared</h3><ul style="text-align: left; margin: 15px 0;">';
1558
1559 for (var key in clearedData) {
1560 var displayName = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
1561 details += '<li><strong>' + displayName + ':</strong> ' + clearedData[key] + '</li>';
1562 }
1563
1564 details += '</ul><p style="color: #666; font-size: 13px;">This data has been permanently removed. You can safely reconnect with a new or existing ' + getPluginName() + ' account.</p></div>';
1565
1566 showConnectInfo('📋 Reset Details', details, [{
1567 text: '�
1568 Got it',
1569 action: function () {
1570 hideConnectStatus();
1571 // Highlight the main connect button briefly to guide user attention
1572 var $mainButton = $('#connect-searchatlas-btn');
1573 if ($mainButton.length > 0) {
1574 $mainButton.addClass('metasync-pulse');
1575 setTimeout(function () {
1576 $mainButton.removeClass('metasync-pulse');
1577 }, 2000);
1578 }
1579 },
1580 primary: true
1581 }]);
1582 }
1583
1584 /**
1585 * Update UI elements when account is disconnected
1586 */
1587 function updateUIForDisconnectedState() {
1588 // a cached manual-sync cooldown must not survive disconnect —
1589 // it keeps the Sync Now button locked after the user reconnects even
1590 // though the server-side cooldown stamp was cleared with the auth data.
1591 localStorage.removeItem('metasync_manual_sync_throttle_expires');
1592 refreshManualSyncCooldownUI();
1593 $('#sendAuthToken').prop('disabled', false).removeClass('is-throttled').text('🔄 Sync Now');
1594
1595 // Update API key field placeholder
1596 // Reset the masked API key display to the "Not configured" state
1597 setSearchAtlasMaskedDisplay('');
1598
1599 // �
1600 Clear OTTO Pixel UUID field
1601 $('input[name="metasync_options[general][otto_pixel_uuid]"]').val('');
1602
1603 // Note: OTTO SSR is always enabled by default, no checkbox to uncheck
1604
1605 // Remove synced indicator from API key field
1606 $('.metasync-sa-connect-container').find('span:contains("✓ Synced")').remove();
1607 $('label[for="searchatlas-api-key"]').find('span').remove(); // Remove any status spans
1608
1609 // Update header status indicator to "Not Synced"
1610 updateHeaderStatus(false, 'Not Synced', 'Missing ' + getPluginName() + ' API key or ' + getOttoName() + ' UUID');
1611
1612 // Update metaSync object for JavaScript state tracking
1613 if (typeof metaSync !== 'undefined') {
1614 metaSync.searchatlas_api_key = false;
1615 metaSync.otto_pixel_uuid = '';
1616 metaSync.is_connected = false;
1617 }
1618
1619 // Update descriptions to reflect disconnected state
1620 $('.metasync-sa-connect-description').html(
1621 'Connect your ' + getPluginName() + ' account with one click. This will automatically configure your API key below and enable all plugin features.'
1622 );
1623
1624 // Clear timestamp display if it exists
1625 $('#sendAuthTokenTimestamp').fadeOut(300);
1626
1627 // Header status already updated above - no need for additional connection status call
1628
1629 console.log('🔄 UI updated to reflect disconnected state - cleared API key, OTTO UUID, and OTTO enable checkbox');
1630
1631 // Show clean success message without duplicate connect button
1632 setTimeout(function () {
1633 showConnectSuccess('�
1634 Account Disconnected',
1635 'Your ' + getPluginName() + ' authentication has been completely reset. Use the "Connect to ' + getPluginName() + '" button above to reconnect.',
1636 [{
1637 text: '�
1638 Got it',
1639 action: function () {
1640 hideConnectStatus();
1641 },
1642 primary: true
1643 }]
1644 );
1645 }, 500); // Shorter delay since no action is needed
1646 }
1647
1648 /**
1649 * Update UI elements when account is connected/authenticated
1650 * Complementary function to updateUIForDisconnectedState()
1651 */
1652 function updateUIForConnectedState(maskedKey, ottoPixelUuid) {
1653 // Update the read-only masked API key display. Only the masked value is
1654 // ever shown — the cleartext key is never rendered into the DOM.
1655 setSearchAtlasMaskedDisplay(maskedKey || '••••••••');
1656
1657 // �
1658 Update OTTO Pixel UUID field
1659 if (ottoPixelUuid) {
1660 $('input[name="metasync_options[general][otto_pixel_uuid]"]').val(ottoPixelUuid);
1661 }
1662
1663 // Note: OTTO SSR is always enabled by default, no checkbox needed
1664
1665 // Update header status indicator based on UUID presence
1666 if (ottoPixelUuid) {
1667 updateHeaderStatus(true, 'Synced', 'Authentication completed - heartbeat sync will be validated on next page load');
1668 } else {
1669 updateHeaderStatus(false, 'Warning', 'Connected but ' + getOttoName() + ' UUID is missing — deploys will not work. Please reconnect.');
1670 }
1671
1672 // Update metaSync object for JavaScript state tracking
1673 if (typeof metaSync !== 'undefined') {
1674 metaSync.searchatlas_api_key = true;
1675 metaSync.is_connected = true;
1676 if (ottoPixelUuid) {
1677 metaSync.otto_pixel_uuid = ottoPixelUuid;
1678 }
1679 }
1680
1681 // Update descriptions to reflect connected state
1682 $('.metasync-sa-connect-description').html(
1683 'Your ' + getPluginName() + ' account is connected and synced successfully. All plugin features are now enabled.'
1684 );
1685
1686 console.log('�
1687 UI updated to reflect connected state - API key set, OTTO UUID set (SSR always enabled)');
1688 }
1689
1690 function addClassTableRowLocalSEO() {
1691 if (document.getElementsByClassName('form-table') && document.getElementById('local_seo_person_organization')) {
1692 const myElement = document.getElementsByTagName('tr');
1693
1694 for (let i = 0; i < myElement.length; i++) {
1695 myElement[i].classList.add('metasync-seo-' + (i + 10));
1696 }
1697 }
1698 }
1699
1700 function addClassTableRowSiteInfo() {
1701 if (document.getElementsByClassName('form-table') && document.getElementById('site_info_type')) {
1702 const myElement = document.getElementsByTagName('tr');
1703
1704 for (let i = 0; i < myElement.length; i++) {
1705 myElement[i].classList.add('metasync-site-info-' + (i + 10));
1706 }
1707 }
1708 }
1709
1710 function uploadMedia(title, text, input, src, closeBtn) {
1711
1712 var mediaUploader;
1713
1714 // If the uploader object has already been created, reopen the dialog
1715 if (mediaUploader) {
1716 mediaUploader.open();
1717 return;
1718 }
1719 // Extend the wp.media object
1720 mediaUploader = wp.media.frames.file_frame = wp.media({
1721 title: title,
1722 button: {
1723 text: text
1724 }, multiple: false
1725 });
1726
1727 // When a file is selected, grab the URL and set it as the text field's value
1728 mediaUploader.on('select', function () {
1729 var attachment = mediaUploader.state().get('selection').first().toJSON();
1730 jQuery('#' + input).val(attachment.id);
1731 jQuery('#' + src).attr('src', attachment.url);
1732 jQuery('#' + src).attr('width', 300);
1733 jQuery('#' + closeBtn).attr('type', 'button');
1734 jQuery('#' + src).show();
1735 jQuery('#' + closeBtn).show();
1736 });
1737 // Open the uploader dialog
1738 mediaUploader.open();
1739 }
1740
1741 function getLocalSeoOnLoadPage() {
1742 if (document.getElementsByClassName('form-table') && document.getElementById('local_seo_person_organization')) {
1743 var $type = $('#local_seo_person_organization').val();
1744 const classes = ['17', '18', '19', '20', '21', '24', '25'];
1745 if ($type === 'Person') {
1746 for (let i = 0; i < classes.length; i++) {
1747 $('.metasync-seo-' + classes[i]).hide();
1748 }
1749 $('.metasync-seo-15').show();
1750 } else {
1751 for (let i = 0; i < classes.length; i++) {
1752 $('.metasync-seo-' + classes[i]).show();
1753 }
1754 $('.metasync-seo-15').hide();
1755 }
1756 }
1757 }
1758
1759 function siteInfoOnLoadPage() {
1760 if (document.getElementsByClassName('form-table') && document.getElementById('site_info_type')) {
1761 var $type = $('#site_info_type').val();
1762 const classes = ['18', '19'];
1763 if ($type === 'blog' || $type === 'portfolio' || $type === 'otherpersonal') {
1764 for (let i = 0; i < classes.length; i++) {
1765 $('.metasync-site-info-' + classes[i]).hide();
1766 }
1767 } else {
1768 for (let i = 0; i < classes.length; i++) {
1769 $('.metasync-site-info-' + classes[i]).show();
1770 }
1771 }
1772 }
1773 }
1774
1775 function deleteTime() {
1776 $(this).parent().remove();
1777 }
1778
1779 function hideElementById(id) {
1780 if ($('#' + id)) {
1781 $('#' + id).hide();
1782 }
1783 }
1784
1785 function removeValueById(id) {
1786 if ($('#' + id)) {
1787 $('#' + id).val('');
1788 }
1789 }
1790
1791 function updateAnalyticsConsentStatus() {
1792 var $toggle = $('#metasync_analytics_opt_in');
1793 var $status = $('.metasync-consent-status');
1794 if (!$toggle.length || !$status.length) {
1795 return;
1796 }
1797
1798 var label = $toggle.is(':checked') ? $status.data('enabled-label') : $status.data('disabled-label');
1799 $status.text(label);
1800 }
1801
1802 $(document).on('change', '#metasync_analytics_opt_in', updateAnalyticsConsentStatus);
1803
1804 $(function () {
1805 updateAnalyticsConsentStatus();
1806 $('#addNewTime').on('click', function () {
1807 $('#daysTime').append(
1808 '<li>' +
1809 '<select name="metasync_options[localseo][days][]">' +
1810 '<option value="Monday">Monday</option>' +
1811 '<option value="Tuseday">Tuseday</option>' +
1812 '<option value="Wednesday">Wednesday</option>' +
1813 '<option value="Thursday">Thursday</option>' +
1814 '<option value="Friday">Friday</option>' +
1815 '<option value="Saturday">Saturday</option>' +
1816 '<option value="Sunday">Sunday</option>' +
1817 '</select>' +
1818 '<input type="text" name="metasync_options[localseo][times][]">' +
1819 '<button id="timeDelete">Delete</button>' +
1820 '</li>');
1821 return;
1822 });
1823 $(document).on('click', '#timeDelete', deleteTime);
1824 });
1825
1826 function deleteNumber() {
1827 $(this).parent().remove();
1828 }
1829
1830 $(function () {
1831 $('#addNewNumber').on('click', function () {
1832 $('#phone-numbers').append(
1833 '<li>' +
1834 '<select name="metasync_options[localseo][phonetype][]">' +
1835 '<option value="Customer Service">Customer Service</option>' +
1836 '<option value="Technical Support">Technical Support</option>' +
1837 '<option value="Billing Support">Billing Support</option>' +
1838 '<option value="Bill Payment">Bill Payment</option>' +
1839 '<option value="Sales">Sales</option>' +
1840 '<option value="Reservations">Reservations</option>' +
1841 '<option value="Credit Card Support">Credit Card Support</option>' +
1842 '<option value="Emergency">Emergency</option>' +
1843 '<option value="Baggage Tracking">Baggage Tracking</option>' +
1844 '<option value="Roadside Assistance">Roadside Assistance</option>' +
1845 '<option value="Package Tracking">Package Tracking</option>' +
1846 '</select>' +
1847 '<input type="text" name="metasync_options[localseo][phonenumber][]">' +
1848 '<button id="number-delete">Delete</button>' +
1849 '</li>');
1850 return;
1851 });
1852 $(document).on('click', '#number-delete', deleteNumber);
1853 });
1854
1855 $(function () {
1856
1857 setToken();
1858
1859 $('body').on('click', '#wp_metasync_sync', function (e) {
1860 e.preventDefault();
1861 metasync_syncPostsAndPages();
1862 });
1863 $('body').on('click', '#metasync_settings_genkey_btn', function () {
1864 $('#apikey').val(metasyncGenerateAPIKey());
1865 });
1866 $('body').on('click', '#lgloginbtn', function () {
1867 // Hide any existing error messages first
1868 $('#lgerror').addClass('hidden').hide();
1869
1870 if ($('#lgusername').val() === '' || $('#lgpassword').val() === '') {
1871 $('.input.lguser').toggleClass('hidden');
1872 } else {
1873 metasyncLGLogin($('#lgusername').val(), $('#lgpassword').val());
1874 }
1875 });
1876
1877 // Enhanced SSO Connect button event handler
1878 // Aggressive event binding that overrides dashboard.js interference
1879 $('body').off('click', '#connect-searchatlas-btn').on('click', '#connect-searchatlas-btn', function (e) {
1880
1881 // Aggressively prevent dashboard interference
1882 preventDashboardInterference($(this));
1883
1884 // Only proceed if button is not in SSO process
1885 if (!$(this).hasClass('connecting') && !$(this).hasClass('authenticating')) {
1886 e.preventDefault();
1887 e.stopPropagation();
1888
1889
1890 handleSearchAtlasConnect();
1891 } else {
1892 }
1893 });
1894
1895 // Also add a direct event listener as backup
1896 setTimeout(function () {
1897 var $btn = $('#connect-searchatlas-btn');
1898 if ($btn.length > 0) {
1899 $btn[0].addEventListener('click', function (e) {
1900 e.preventDefault();
1901 e.stopPropagation();
1902
1903 // Force enable the button and clean classes
1904 preventDashboardInterference($(this));
1905
1906 if (!$(this).hasClass('connecting') && !$(this).hasClass('authenticating')) {
1907 handleSearchAtlasConnect();
1908 }
1909 }, true); // Use capture phase to get event before other handlers
1910 }
1911 }, 500);
1912
1913 // Monitor button state changes and fix interference
1914 setTimeout(function () {
1915 var $btn = $('#connect-searchatlas-btn');
1916 if ($btn.length > 0) {
1917 // Store original button state for restoration
1918 var buttonState = {
1919 disabled: $btn.prop('disabled'),
1920 style: $btn.attr('style'),
1921 pointerEvents: $btn.css('pointer-events'),
1922 zIndex: $btn.css('z-index'),
1923 position: $btn.css('position'),
1924 classes: $btn.attr('class')
1925 };
1926
1927 // Monitor for unwanted changes to the button
1928 var observer = new MutationObserver(function (mutations) {
1929 mutations.forEach(function (mutation) {
1930 if (mutation.type === 'attributes') {
1931 // Monitor for unwanted attribute changes
1932
1933 // Fix dashboard interference automatically
1934 if (mutation.attributeName === 'class' && $btn.hasClass('dashboard-loading')) {
1935 preventDashboardInterference($btn);
1936 }
1937
1938 if (mutation.attributeName === 'disabled' && $btn.prop('disabled') && !$btn.hasClass('connecting')) {
1939 preventDashboardInterference($btn);
1940 }
1941 }
1942 });
1943 });
1944
1945 observer.observe($btn[0], {
1946 attributes: true,
1947 attributeOldValue: true,
1948 attributeFilter: ['class', 'disabled', 'style']
1949 });
1950 }
1951 }, 1000);
1952
1953 // SSO Reset button event handler
1954 $('body').on('click', '#reset-searchatlas-auth', function (e) {
1955 e.preventDefault();
1956 handleSearchAtlasResetAuth();
1957 });
1958
1959 $('body').on('click', '#local_seo_logo_close_btn', function () {
1960 removeValueById('local_seo_logo');
1961 hideElementById('local_seo_business_logo');
1962 hideElementById('local_seo_logo_close_btn');
1963 });
1964
1965 $('body').on('click', '#site_google_logo_close_btn', function () {
1966 removeValueById('site_google_logo');
1967 hideElementById('site_google_logo_img');
1968 hideElementById('site_google_logo_close_btn');
1969 });
1970
1971 $('body').on('click', '#site_social_image_close_btn', function () {
1972 removeValueById('site_social_share_image');
1973 hideElementById('site_social_share_img');
1974 hideElementById('site_social_image_close_btn');
1975 });
1976
1977 $('body').on('click', '#logo_upload_button', function () {
1978 uploadMedia('Logo', 'Add', 'local_seo_logo', 'local_seo_business_logo', 'local_seo_logo_close_btn');
1979 });
1980
1981 $('body').on('click', '#google_logo_btn', function () {
1982 uploadMedia('Site Google Logo', 'Add', 'site_google_logo', 'site_google_logo_img', 'site_google_logo_close_btn');
1983 });
1984
1985 $('body').on('click', '#social_share_image_btn', function () {
1986 uploadMedia('Site Social Share Image', 'Add', 'site_social_share_image', 'site_social_share_img', 'site_social_image_close_btn');
1987 });
1988
1989 $('body').on('click', '#robots_common1', function () {
1990 $('#robots_common1').prop('checked', true);
1991 $('#robots_common2').prop('checked', false);
1992 });
1993
1994 $('body').on('click', '#robots_common2', function () {
1995 $('#robots_common1').prop('checked', false);
1996 $('#robots_common2').prop('checked', true);
1997 });
1998
1999 addClassTableRowLocalSEO();
2000
2001 addClassTableRowSiteInfo();
2002
2003 getLocalSeoOnLoadPage();
2004
2005 siteInfoOnLoadPage();
2006
2007 $('#local_seo_person_organization').change(function () {
2008 const classes = ['17', '18', '19', '20', '21', '24', '25'];
2009 if (this.value === 'Person') {
2010 for (let i = 0; i < classes.length; i++) {
2011 $('.metasync-seo-' + classes[i]).hide();
2012 }
2013 $('.metasync-seo-15').show();
2014 } else {
2015 for (let i = 0; i < classes.length; i++) {
2016 $('.metasync-seo-' + classes[i]).show();
2017 }
2018 $('.metasync-seo-15').hide();
2019 }
2020 });
2021
2022 $('#site_info_type').change(function () {
2023 const classes = ['18', '19'];
2024 if (this.value === 'blog' || this.value === 'portfolio' || this.value === 'otherpersonal') {
2025 for (let i = 0; i < classes.length; i++) {
2026 $('.metasync-site-info-' + classes[i]).hide();
2027 }
2028 } else {
2029 for (let i = 0; i < classes.length; i++) {
2030 $('.metasync-site-info-' + classes[i]).show();
2031 }
2032 }
2033 });
2034
2035 $('#metasync-giapi-response').hide();
2036
2037 $('body').on('click', '#metasync-btn-send', function () {
2038
2039 var url = $('#metasync-giapi-url');
2040 var action = $('input[type="radio"]:checked');
2041 var response = $('#metasync-giapi-response');
2042
2043 var urls = url.val().split('\n').filter(Boolean);
2044
2045 var urls_str = urls[0];
2046 var is_bulk = false;
2047 if (urls.length > 1) {
2048 urls_str = urls;
2049 is_bulk = true;
2050 }
2051
2052 jQuery.ajax({
2053 method: 'POST',
2054 url: 'admin-ajax.php',
2055 data: {
2056 action: 'metasync_send_giapi',
2057 nonce: metaSync.nonce,
2058 metasync_giapi_url: url.val(),
2059 metasync_giapi_action: action.val()
2060 }
2061 })
2062 .always(function (info) {
2063
2064 response.show();
2065
2066 $('.result-action').html('<strong>' + action.val() + '</strong>' + ' <br> ' + urls_str);
2067
2068 if (!is_bulk) {
2069 if (typeof info.error !== 'undefined') {
2070 $('.result-status-code').text(info.error.code).siblings('.result-message').text(info.error.message);
2071 } else {
2072 var d = new Date();
2073 $('.result-status-code').text('Success').siblings('.result-message').text(d.toString());
2074 }
2075 } else {
2076 $('.result-status-code').text('Success').siblings('.result-message').text('Success');
2077 if (typeof info.error !== 'undefined') {
2078 $('.result-status-code').text(info.error.code).siblings('.result-message').text(info.error.message);
2079 } else {
2080 $.each(info, function (index, val) {
2081
2082 if (typeof val.error !== 'undefined') {
2083 var error_code = '';
2084 if (typeof val.error.code !== 'undefined') {
2085 error_code = val.error.code;
2086 }
2087 var error_message = '';
2088 if (typeof val.error.message !== 'undefined') {
2089 error_message = val.error.message;
2090 }
2091 $('.result-status-code').text(error_code).siblings('.result-message').text(val.error.message);
2092 }
2093 });
2094 }
2095 }
2096 });
2097 });
2098
2099 $('body').on('click', '#cancel-redirection', function () {
2100 $('#add-redirection-form').hide();
2101 $('#add-redirection').focus();
2102 });
2103
2104 $('body').on('click', '.redirect_type', function () {
2105 if ($(this).val() === '410' || $(this).val() === '451') {
2106 $('#destination_url').val('');
2107 $('#destination').hide();
2108 } else {
2109 $('#destination').show();
2110 }
2111 });
2112
2113 if ($('#post_redirection').is(':checked')) {
2114 $('.hide').fadeIn('slow');
2115 }
2116 $('body').on('change', '#post_redirection', function () {
2117 if (this.checked) {
2118 $('.hide').fadeIn('slow');
2119 } else {
2120 $('.hide').fadeOut('slow');
2121 }
2122 });
2123
2124 $(document).ready(function () {
2125 if ($('#post_redirection').is(':checked')
2126 && ($('#post_redirection_type').val() === '410'
2127 || $('#post_redirection_type').val() === '451')) {
2128 $('#post_redirect_url').hide();
2129 }
2130 });
2131
2132 $('#post_redirection_type').change(function () {
2133 if ($('#post_redirection').is(':checked')
2134 && ($(this).val() === '410'
2135 || $(this).val() === '451')) {
2136 $('#post_redirect_url').hide();
2137 } else {
2138 $('#post_redirect_url').show();
2139 }
2140 });
2141
2142 });
2143
2144 $(function () {
2145 var psconsole = $('#error-code-box');
2146 if (psconsole.length) {
2147 psconsole.scrollTop(psconsole[0].scrollHeight - psconsole.height());
2148 }
2149 });
2150
2151 $(function () {
2152 $('#copy-clipboard-btn').on('click', function () {
2153 var hiddenInput = document.createElement('input');
2154 hiddenInput.setAttribute('value', document.getElementById('error-code-box').value);
2155 document.body.appendChild(hiddenInput);
2156 hiddenInput.select();
2157 document.execCommand('copy');
2158 document.body.removeChild(hiddenInput);
2159 });
2160 });
2161
2162 function dateFormat() {
2163 var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
2164 var m = new Date();
2165 return months[m.getMonth()] + ' ' + ('0' + m.getDate()).slice(-2) + ', ' + m.getFullYear() + ' ' + (m.getHours() > 12 ? '0' + m.getHours() % 12 : '0' + m.getHours()).slice(-2) + ':' + ('0' + m.getMinutes()).slice(-2) + ':' + ('0' + m.getSeconds()).slice(-2) + ' ' + (m.getHours() > 12 ? 'PM' : 'AM');
2166 }
2167
2168 function sendCustomerParams(is_hb = false) {
2169 // Only show alerts for manual sync (not heartbeat)
2170 const showAlerts = !is_hb;
2171
2172 // Remove any existing sync-related notices
2173 if (showAlerts) {
2174 $('.metasync-sync-notice, .metasync-sync-error').remove();
2175 }
2176
2177 // DEBUG: Log request parameters
2178 const requestData = {
2179 action: 'metasync_send_customer_params',
2180 nonce: metaSync.nonce,
2181 is_heart_beat : is_hb
2182 };
2183
2184 jQuery.ajax({
2185 type: 'post',
2186 url: 'admin-ajax.php',
2187 data: requestData,
2188 beforeSend: function () {
2189 if (showAlerts) {
2190 // Show loading state on button
2191 $('#sendAuthToken').prop('disabled', true).html('🔄 Syncing...');
2192 }
2193 },
2194 success: function (response) {
2195 // Reset button state
2196 if (showAlerts) {
2197 $('#sendAuthToken').prop('disabled', false).html('🔄 Sync Now');
2198 }
2199
2200 if ($('#searchatlas-api-key-display').length > 0 && !searchAtlasKeyConfigured()) {
2201 if (!is_hb) {
2202 $('#sendAuthTokenTimestamp').html('Please save your ' + getPluginName() + ' API key');
2203 $('#sendAuthTokenTimestamp').css({ color: 'red' });
2204
2205 // Remove stale PHP-rendered "✓ Synced" label.
2206 $('label[for="searchatlas-api-key"]').find('span:contains("Synced")').remove();
2207
2208 updateHeaderStatus(false, 'Not Synced', 'Not Synced - API key required');
2209 }
2210
2211 if (showAlerts) {
2212 showSyncError('⚠️ API Key Required', 'Please save your ' + getPluginName() + ' API key in the settings above before syncing.');
2213 }
2214
2215 } else if (response && response.throttled) {
2216 // Compute expiry locally from a duration (remaining_seconds) so server/client
2217 // clock drift cannot lock the user out or let them bypass the throttle.
2218 var remainingSeconds = (typeof response.remaining_seconds === 'number')
2219 ? response.remaining_seconds
2220 : ((response.remaining_minutes || 5) * 60);
2221 var expiryMs = Date.now() + (remainingSeconds * 1000);
2222 var remainingMinutes = Math.max(1, Math.ceil(remainingSeconds / 60));
2223
2224 if (showAlerts) {
2225 localStorage.setItem('metasync_manual_sync_throttle_expires', String(expiryMs));
2226 refreshManualSyncCooldownUI();
2227 }
2228
2229 // Throttle is transient — don't change the connection badge.
2230 // The toast notification already informs the user.
2231
2232 if (showAlerts) {
2233 showSyncError('⏰ Request Throttled', response.message || 'Please wait ' + remainingMinutes + ' minutes before making another sync request.');
2234 }
2235
2236 } else if (response && response.detail) {
2237 if (!is_hb) {
2238 $('#sendAuthTokenTimestamp').html('Please provide a valid ' + getPluginName() + ' API key');
2239 $('#sendAuthTokenTimestamp').css({ color: 'red' });
2240
2241 // Remove stale PHP-rendered "✓ Synced" label — key is invalid on the server.
2242 $('label[for="searchatlas-api-key"]').find('span:contains("Synced")').remove();
2243 }
2244
2245 // No is_hb guard here — genuinely invalid keys should always flip the badge
2246 updateHeaderStatus(false, 'Not Synced', 'Not Synced - Invalid API key');
2247
2248 if (showAlerts) {
2249 showSyncError('❌ Invalid API Key', 'Please provide a valid ' + getPluginName() + ' API key.');
2250 }
2251
2252 } else if (response === null || !response.id) {
2253 // Update header status to "Not Synced" after failed sync
2254 if (!is_hb) {
2255 updateHeaderStatus(false, 'Not Synced', 'Not Synced - Data synchronization failed');
2256 }
2257
2258 if (showAlerts) {
2259 showSyncError('❌ Sync Failed', 'Something went wrong during synchronization. Please check your connection and try again.');
2260 }
2261 // Keep existing commented behavior for timestamp
2262
2263 } else {
2264 var dateString = dateFormat();
2265 $('#sendAuthTokenTimestamp').html(dateString);
2266 $('#sendAuthTokenTimestamp').css({ color: 'green' });
2267 // Server starts a 5-minute manual-sync cooldown after a successful sync.
2268 // Reflect that on the client so the button shows the cooldown across refreshes
2269 // instead of misleadingly saying "Sync Now" only to be rejected on next click.
2270 if (!is_hb) {
2271 var successCooldownMs = 5 * 60 * 1000;
2272 localStorage.setItem('metasync_manual_sync_throttle_expires', String(Date.now() + successCooldownMs));
2273 refreshManualSyncCooldownUI();
2274 }
2275
2276 // Update header status — check if UUID is present before declaring fully synced
2277 var hasOttoUuid = metaSync.otto_pixel_uuid && metaSync.otto_pixel_uuid.trim() !== '';
2278 if (hasOttoUuid) {
2279 updateHeaderStatus(true, 'Synced', 'Synced - Data synchronization completed successfully');
2280 } else {
2281 updateHeaderStatus(false, 'Warning', 'Connected but ' + getOttoName() + ' UUID is missing — deploys will not work. Please reconnect.');
2282 }
2283
2284 if (showAlerts) {
2285 showSyncSuccess('�
2286 Sync Complete', 'Your categories and user data have been successfully synchronized with ' + getPluginName() + '.');
2287 }
2288 }
2289 },
2290 error: function (xhr, status, error) {
2291 // Update header status to "Not Synced" for network errors
2292 if (!is_hb) {
2293 updateHeaderStatus(false, 'Not Synced', 'Not Synced - Network error during sync');
2294 }
2295
2296 // Reset button state
2297 if (showAlerts) {
2298 $('#sendAuthToken').prop('disabled', false).html('🔄 Sync Now');
2299 showSyncError('❌ Network Error', 'Failed to connect to ' + getPluginName() + '. Please check your internet connection and try again.');
2300 }
2301 }
2302 });
2303 }
2304
2305 /**
2306 * Show sync success notification (wrapper for consolidated function)
2307 */
2308 function showSyncSuccess(title, message) {
2309 showPluginNotice('success', title, message, 'metasync-sync-notice', 4000);
2310 }
2311
2312 /**
2313 * Show sync error notification (wrapper for consolidated function)
2314 */
2315 function showSyncError(title, message) {
2316 showPluginNotice('error', title, message, 'metasync-sync-error', 0);
2317 }
2318
2319 function clear_otto_caches() {
2320 jQuery.ajax({
2321 url: ajaxurl,
2322 type: 'GET',
2323 data: {
2324 action: 'metasync_clear_otto_cache',
2325 clear_otto_cache: 1,
2326 nonce: metaSync.clear_otto_cache_nonce || ''
2327 },
2328 success: function (response) {
2329 const now = new Date();
2330 $('#clear_otto_caches').text('Cache Cleared ' + now.toLocaleTimeString());
2331 console.log('Cleared SSR Caches');
2332 }
2333 });
2334 }
2335
2336 // Holds the active manual-sync countdown interval so we can replace it
2337 // instead of stacking multiple timers if refreshManualSyncCooldownUI()
2338 // is called more than once (e.g. throttled response then refresh).
2339 var _manualSyncCountdownInterval = null;
2340
2341 /**
2342 * Read the manual-sync throttle expiry (client-time ms) from localStorage
2343 * and apply UI state: disable the button, show "Throttled (Nm)", and tick
2344 * every 15s. Idempotent — safe to call repeatedly.
2345 */
2346 function refreshManualSyncCooldownUI() {
2347 var $btn = $('#sendAuthToken');
2348 if ($btn.length === 0) {
2349 return;
2350 }
2351 if (_manualSyncCountdownInterval) {
2352 clearInterval(_manualSyncCountdownInterval);
2353 _manualSyncCountdownInterval = null;
2354 }
2355 var stored = localStorage.getItem('metasync_manual_sync_throttle_expires');
2356 if (!stored) {
2357 return;
2358 }
2359 var storedExpiry = parseInt(stored, 10);
2360 if (!storedExpiry || isNaN(storedExpiry)) {
2361 localStorage.removeItem('metasync_manual_sync_throttle_expires');
2362 return;
2363 }
2364 var nowMs = Date.now();
2365 if (storedExpiry <= nowMs) {
2366 localStorage.removeItem('metasync_manual_sync_throttle_expires');
2367 return;
2368 }
2369 var remainingMinutes = Math.max(1, Math.ceil((storedExpiry - nowMs) / 60000));
2370 // addClass('is-throttled') drives the visual state via CSS so it
2371 // doesn't depend on the disabled attribute being preserved by other
2372 // code paths in the AJAX callback chain (which strip it on success).
2373 $btn.prop('disabled', true).addClass('is-throttled').text('⏰ Throttled (' + remainingMinutes + 'm)');
2374 _manualSyncCountdownInterval = setInterval(function () {
2375 var currentNowMs = Date.now();
2376 if (storedExpiry <= currentNowMs) {
2377 clearInterval(_manualSyncCountdownInterval);
2378 _manualSyncCountdownInterval = null;
2379 localStorage.removeItem('metasync_manual_sync_throttle_expires');
2380 $btn.prop('disabled', false).removeClass('is-throttled').text('🔄 Sync Now');
2381 $('#sendAuthTokenTimestamp').text('Ready to sync').css({ color: 'green' });
2382 // Cooldown is over — clear the lingering "Request Throttled" admin
2383 // notice at the top of the page. Without this the button resets to
2384 // "Sync Now" but the red throttled bar stays visible (QA).
2385 $('.metasync-sync-notice, .metasync-sync-error').remove();
2386 return;
2387 }
2388 remainingMinutes = Math.max(1, Math.ceil((storedExpiry - currentNowMs) / 60000));
2389 $btn.text('⏰ Throttled (' + remainingMinutes + 'm)');
2390 }, 15000);
2391 }
2392
2393 jQuery(document).ready(function () {
2394
2395 // sendCustomerParams();
2396 $('#sendAuthToken').on('click', function (e) {
2397 e.preventDefault();
2398 sendCustomerParams();
2399
2400 });
2401
2402 // Restore manual-sync throttle countdown state across page refreshes.
2403 refreshManualSyncCooldownUI();
2404
2405 // handle otto clear cache button
2406 $('#clear_otto_caches').on('click', function (e){
2407 e.preventDefault();
2408 clear_otto_caches();
2409 });
2410
2411 // Handle General Setting Page form Submit
2412 $('#metaSyncGeneralSetting').on('submit', function (e) {
2413 // Check if this is a "Clear Error Logs" submission
2414 var formData = $(this).serialize();
2415 if (formData.indexOf('clear_log=yes') !== -1) {
2416 // This is a clear error logs submission - allow normal HTML form submission
2417 console.log('Clear Error Logs form detected - allowing HTML submission');
2418 return true; // Let the form submit normally
2419 }
2420
2421 // Check if this is a "Plugin Access Roles" submission - allow normal HTML form submission
2422 if (formData.indexOf('save_plugin_access_roles=yes') !== -1) {
2423 console.log('Plugin Access Roles form detected - allowing HTML submission');
2424 return true; // Let the form submit normally
2425 }
2426
2427 e.preventDefault(); // Prevent the default form submission for regular settings
2428
2429 // Immediate client-side validation of the white-label branding
2430 // fields (the server-side contract stays authoritative): block the
2431 // submit and show an error beside each invalid field instead of
2432 // letting an invalid value reach the server and get rejected there.
2433 var brandingValidationErrors = validateBrandingFieldsInDom();
2434 if (brandingValidationErrors.length > 0) {
2435 var $brandingErrorNotice = $('<div>').addClass('notice notice-error metasync-error-wrap');
2436 var $brandingErrorList = $('<ul>');
2437 brandingValidationErrors.forEach(function (errorMessage) {
2438 $brandingErrorList.append($('<li>').text(errorMessage));
2439 });
2440 $brandingErrorNotice.append($brandingErrorList);
2441 $('.metasync-error-wrap').remove();
2442 $('#metaSyncGeneralSetting').before($brandingErrorNotice);
2443 $('html, body').animate({ scrollTop: 0 }, 'slow');
2444 return;
2445 }
2446
2447 var actionField = $(this).find('input[name="action"]');
2448 var optionPage= $(this).find('input[name="option_page"]');
2449 var wpHttpReferer= $(this).find('input[name="_wp_http_referer"]');
2450 var wpnonce= $(this).find('input[name="_wpnonce"]');
2451 if(actionField.length > 0) {
2452 actionField.remove(); // Remove the action field if it exists
2453 optionPage.remove(); // Remove the action field if it exists
2454 wpHttpReferer.remove(); // Remove the action field if it exists
2455 wpnonce.remove(); // Remove the action field if it exists
2456 }
2457 // Re-serialize the form data after removing unwanted fields
2458 formData = $(this).serialize();
2459
2460 // Check if whitelabel data is in form data, if not add it manually
2461 if (formData.indexOf('whitelabel') === -1) {
2462 // Manually collect and add ALL whitelabel fields using helper function
2463 var whitelabelData = collectWhitelabelFields();
2464 if (whitelabelData) {
2465 formData += '&' + whitelabelData;
2466 }
2467 }
2468
2469 // Get current tab from URL
2470 var urlParams = new URLSearchParams(window.location.search);
2471 var currentTab = urlParams.get('tab') || 'general';
2472
2473 $.ajax({
2474 url: metaSync.ajax_url, // The AJAX URL provided by WordPress
2475 type: 'POST',
2476 data: formData + '&action=meta_sync_save_settings&active_tab=' + encodeURIComponent(currentTab), // Add the action and current tab
2477 success: function (response) {
2478 // Handle success response
2479 if(response.success){
2480 // Per-field whitelabel validation issues: the save succeeded for
2481 // other fields, so show a warning notice and stay on the page so the
2482 // user can see which values were rejected.
2483 const wlErrors = response.data && response.data.whitelabel_errors;
2484 if (wlErrors && Object.keys(wlErrors).length > 0) {
2485 var $warnNotice = $('<div>').addClass('notice notice-warning metasync-error-wrap');
2486 var $warnList = $('<ul>');
2487 Object.keys(wlErrors).forEach(function (fieldKey) {
2488 $warnList.append($('<li>').text(wlErrors[fieldKey]));
2489 });
2490 $warnNotice.append($('<p>').text('Settings saved, but some values were not applied:'));
2491 $warnNotice.append($warnList);
2492 $('.metasync-error-wrap').remove();
2493 $('#metaSyncGeneralSetting').before($warnNotice);
2494 $('html, body').animate({ scrollTop: 0 }, 'slow');
2495 return;
2496 }
2497
2498 // get value of input field white_label_plugin_menu_slug
2499 const whiteLableUrl = $('#metaSyncGeneralSetting input[name="metasync_options[general][white_label_plugin_menu_slug]"]').val();
2500 // check condition if it is empty or not and redirect it
2501
2502 // add the tag query to the window location
2503 let tabParam = new URLSearchParams(window.location.search).get('tab');
2504 let tabQuery = tabParam ? '&tab=' + encodeURIComponent(tabParam) : '';
2505
2506 // Prefer the server-sanitized slug so the redirect matches the page
2507 // WordPress actually registered; fall back to the raw input field value.
2508 const savedSlug = response.data && typeof response.data.saved_menu_slug === 'string' ? response.data.saved_menu_slug : null;
2509 const inputSlug = (whiteLableUrl && whiteLableUrl !== '') ? whiteLableUrl : 'searchatlas';
2510 const rawSlug = (savedSlug !== null) ? (savedSlug || 'searchatlas') : inputSlug;
2511 const pageSlug = encodeURIComponent(rawSlug.replace(/[^a-zA-Z0-9_\-]/g, '')) || 'searchatlas';
2512 var redirectUrl = metaSync.admin_url + '?page=' + pageSlug + tabQuery;
2513 try {
2514 var parsedRedirect = new URL(redirectUrl, window.location.origin);
2515 if (parsedRedirect.origin === window.location.origin) {
2516 var navLink = document.createElement('a');
2517 navLink.href = parsedRedirect.href;
2518 navLink.click();
2519 }
2520 } catch (e) { /* invalid URL */ }
2521 }else {
2522 // Handle error response
2523 const errors = response.data?.errors || [];
2524 const fieldErrors = response.data?.field_errors || {};
2525
2526 // Build error notice using DOM methods to avoid XSS
2527 var $errorNotice = $('<div>').addClass('notice notice-error metasync-error-wrap');
2528 if (Array.isArray(errors) && errors.length > 0) {
2529 var $ul = $('<ul>');
2530 errors.forEach(function (err) {
2531 $ul.append($('<li>').text(err));
2532 });
2533 $errorNotice.append($ul);
2534 }
2535
2536 // Remove previous error notices
2537 $('.metasync-error-wrap').remove();
2538
2539 // Insert the error message before the form
2540 $('#metaSyncGeneralSetting').before($errorNotice);
2541
2542 // Field-specific feedback: show the server's per-field
2543 // error beside each rejected white-label branding input.
2544 clearBrandingFieldErrors();
2545 Object.keys(fieldErrors).forEach(function (settingKey) {
2546 showBrandingFieldError(settingKey, fieldErrors[settingKey]);
2547 });
2548
2549 // Scroll to the top to ensure visibility
2550 $('html, body').animate({ scrollTop: 0 }, 'slow');
2551 }
2552
2553 },
2554 error: function (error) {
2555 // Handle error response
2556 alert('There was an error saving the settings.');
2557 console.log(error);
2558 }
2559 });
2560 });
2561
2562 //hook into heartbeat-send: client will send the message 'marco' in the 'client' var inside the data array
2563 jQuery(document).on('heartbeat-send', function (e, data) {
2564 e.preventDefault();
2565
2566 // adding heart beat label
2567 sendCustomerParams(true);
2568 });
2569
2570 //hook into heartbeat-tick: client looks for a 'server' var in the data array and logs it to console
2571 jQuery(document).on('heartbeat-tick', function (e, data) {
2572 // console.log('heartbeat-tick:', data);
2573 // if(data['server'])
2574 // console.log('Server: ' + data['server']);
2575 });
2576
2577 //hook into heartbeat-error: in case of error, let's log some stuff
2578 jQuery(document).on('heartbeat-error', function (e, jqXHR, textStatus, error) {
2579 console.log('BEGIN ERROR');
2580 console.log(textStatus);
2581 console.log(error);
2582 console.log('END ERROR');
2583 });
2584
2585 // Unsaved changes warning functionality
2586 var hasUnsavedChanges = false;
2587 var initialFormData = {};
2588
2589 // Check if we're on the Advanced tab (has its own save buttons per section)
2590 function isAdvancedTab() {
2591 return window.location.href.indexOf('tab=advanced') > -1;
2592 }
2593
2594 // Initialize form change detection
2595 function initializeUnsavedChangesDetection() {
2596 // Skip unsaved changes detection on Advanced tab - it has its own section-specific save buttons
2597 if (isAdvancedTab()) {
2598 return;
2599 }
2600
2601 var $forms = $('#metaSyncGeneralSetting, #metaSyncSeoControlsForm, form[method="post"][action*="options.php"]');
2602
2603 if ($forms.length === 0) {
2604 return; // No forms to track
2605 }
2606
2607 // Store initial form data
2608 $forms.each(function () {
2609 var formId = $(this).attr('id') || 'form_' + Math.random().toString(36).substr(2, 9);
2610 initialFormData[formId] = $(this).serialize();
2611 });
2612
2613 // Track changes on form inputs
2614 $forms.on('input change', 'input, select, textarea', function () {
2615 checkForChanges();
2616 });
2617
2618 // Special handling for media uploads and other dynamic changes
2619 $forms.on('DOMSubtreeModified', function () {
2620 setTimeout(checkForChanges, 100); // Small delay to allow DOM changes to complete
2621 });
2622 }
2623
2624 // Check if form data has changed
2625 function checkForChanges() {
2626 // Skip on Advanced tab
2627 if (isAdvancedTab()) {
2628 return;
2629 }
2630
2631 var $forms = $('#metaSyncGeneralSetting, form[method="post"][action*="options.php"]');
2632 var currentHasChanges = false;
2633
2634 $forms.each(function () {
2635 var formId = $(this).attr('id') || 'form_' + Math.random().toString(36).substr(2, 9);
2636 var currentData = $(this).serialize();
2637
2638 if (initialFormData[formId] && currentData !== initialFormData[formId]) {
2639 currentHasChanges = true;
2640 }
2641 });
2642
2643 hasUnsavedChanges = currentHasChanges;
2644 updateUnsavedChangesIndicator();
2645 }
2646
2647 // Update visual indicator for unsaved changes
2648 function updateUnsavedChangesIndicator() {
2649 var $saveButtons = $('input[type="submit"], button[type="submit"]').filter('[name="submit"], [value*="Save"]');
2650 var $forms = $('#metaSyncGeneralSetting, form[method="post"][action*="options.php"]');
2651
2652 if (hasUnsavedChanges) {
2653 // Add modern visual indicator to save buttons
2654 $saveButtons.each(function () {
2655 if (!$(this).find('.unsaved-indicator').length) {
2656 $(this).prepend('<span class="unsaved-indicator">●</span>');
2657 }
2658 });
2659
2660 // Add visual styling to forms
2661 $forms.addClass('has-unsaved-changes');
2662
2663 // Show sticky notification
2664 showUnsavedChangesNotification();
2665 } else {
2666 // Remove indicators
2667 $saveButtons.find('.unsaved-indicator').remove();
2668 $forms.removeClass('has-unsaved-changes');
2669
2670 // Hide sticky notification
2671 window.hideUnsavedChangesNotification();
2672 }
2673 }
2674
2675 // Show sticky notification for unsaved changes
2676 function showUnsavedChangesNotification() {
2677 var $notification = $('.metasync-unsaved-notification');
2678
2679 if ($notification.length === 0) {
2680 // Create notification if it doesn't exist
2681 var notificationHTML =
2682 '<div class="metasync-unsaved-notification">' +
2683 '<div class="notification-content">' +
2684 '<div class="notification-icon">●</div>' +
2685 '<div class="notification-message">You have unsaved changes</div>' +
2686 '</div>' +
2687 '<div class="notification-actions">' +
2688 '<button class="notification-button primary" onclick="saveChanges()">Save Now</button>' +
2689 '<button class="notification-button" onclick="discardChanges()">Discard</button>' +
2690 '<button class="close-notification" onclick="hideUnsavedChangesNotification()">×</button>' +
2691 '</div>' +
2692 '</div>';
2693
2694 $('body').append(notificationHTML);
2695 $notification = $('.metasync-unsaved-notification');
2696 }
2697
2698 // Show with animation
2699 setTimeout(function () {
2700 $notification.addClass('show');
2701 }, 100);
2702 }
2703
2704 // Hide sticky notification
2705 window.hideUnsavedChangesNotification = function () {
2706 var $notification = $('.metasync-unsaved-notification');
2707 $notification.removeClass('show');
2708 };
2709
2710 // Scroll to save button functionality
2711 window.scrollToSaveButton = function () {
2712 var $saveButton = $('input[type="submit"], button[type="submit"]').filter('[name="submit"], [value*="Save"]').first();
2713 if ($saveButton.length) {
2714 // Hide notification temporarily while scrolling
2715 window.hideUnsavedChangesNotification();
2716
2717 $('html, body').animate({
2718 scrollTop: $saveButton.offset().top - 100
2719 }, 500, function () {
2720 // Add highlight animation
2721 $saveButton.css('animation', 'save-button-highlight 1s ease-in-out');
2722
2723 // Remove animation after it completes
2724 setTimeout(function () {
2725 $saveButton.css('animation', '');
2726 }, 1000);
2727 });
2728 }
2729 };
2730
2731 // Discard changes functionality
2732 window.discardChanges = function () {
2733 if (confirm('Are you sure you want to discard all unsaved changes? This action cannot be undone.')) {
2734 // Reload the page to discard changes
2735 window.location.reload();
2736 }
2737 };
2738
2739
2740
2741 // Warning for in-page navigation (tab links)
2742 $('.metasync-nav-tab, .nav-tab').on('click', function (e) {
2743 if (hasUnsavedChanges) {
2744 var tabName = $(this).text().trim();
2745 var confirmed = confirm('⚠️ Unsaved Changes Alert\n\nYou have unsaved changes that will be lost if you navigate to "' + tabName + '".\n\nWould you like to:\n• Click "Cancel" to stay and save your changes\n• Click "OK" to discard changes and continue');
2746 if (!confirmed) {
2747 e.preventDefault();
2748 return false;
2749 } else {
2750 // If user confirms, clear the unsaved changes state
2751 hasUnsavedChanges = false;
2752 updateUnsavedChangesIndicator();
2753 }
2754 }
2755 });
2756
2757 // Clear unsaved changes flag when form is successfully submitted
2758 $('#metaSyncGeneralSetting').on('submit', function () {
2759 // Form submission handler already exists above, so we just need to listen for successful response
2760 var originalAjaxHandler = $(this).data('events') && $(this).data('events').submit;
2761 });
2762
2763 // Listen for successful form submission to clear the unsaved changes flag
2764 $(document).ajaxSuccess(function (event, xhr, settings) {
2765 if (settings.data && typeof settings.data === 'string' && settings.data.indexOf('action=meta_sync_save_settings') > -1) {
2766 try {
2767 var response = JSON.parse(xhr.responseText);
2768 if (response.success) {
2769 hasUnsavedChanges = false;
2770 updateUnsavedChangesIndicator();
2771 // Update initial form data after successful save
2772 var $forms = $('#metaSyncGeneralSetting, form[method="post"][action*="options.php"]');
2773 $forms.each(function () {
2774 var formId = $(this).attr('id') || 'form_' + Math.random().toString(36).substr(2, 9);
2775 initialFormData[formId] = $(this).serialize();
2776 });
2777 }
2778 } catch (e) {
2779 // Response is not JSON, ignore
2780 }
2781 }
2782 });
2783
2784 // Save Changes function - use AJAX instead of form submission
2785 window.saveChanges = function () {
2786 isSaving = true;
2787
2788 var $form = $('#metaSyncGeneralSetting');
2789 if ($form.length > 0) {
2790 // Immediate client-side validation of the white-label branding
2791 // fields — the floating save bypasses the form submit handler,
2792 // so the same contract is enforced here too. The server-side
2793 // validation stays authoritative. Runs BEFORE the notification
2794 // is removed so a blocked save keeps the persistent unsaved-
2795 // changes cue visible.
2796 var brandingProblems = validateBrandingFieldsInDom();
2797 if (brandingProblems.length > 0) {
2798 var $brandingNotice = $('<div>').addClass('notice notice-error metasync-save-notice').css({ margin: '20px 0', padding: '12px' });
2799 var $brandingList = $('<ul>');
2800 brandingProblems.forEach(function (errorMessage) {
2801 $brandingList.append($('<li>').text(errorMessage));
2802 });
2803 $brandingNotice.append($brandingList);
2804 $('.metasync-save-notice').remove();
2805 var $navWrapperForBranding = $('.metasync-nav-wrapper');
2806 if ($navWrapperForBranding.length > 0) {
2807 $navWrapperForBranding.after($brandingNotice);
2808 } else {
2809 $('.metasync-dashboard-wrap').prepend($brandingNotice);
2810 }
2811 $('html, body').animate({ scrollTop: 0 }, 'slow');
2812 isSaving = false;
2813 return;
2814 }
2815 }
2816
2817 // Completely remove the floating notification immediately when the
2818 // save is actually proceeding (a blocked save left it in place).
2819 var $notification = $('.metasync-unsaved-notification');
2820 if ($notification.length > 0) {
2821 $notification.remove(); // Completely remove from DOM, no animations
2822 }
2823
2824 if ($form.length > 0) {
2825
2826 // Get form data and submit via AJAX
2827 var formData = $form.serialize();
2828
2829 // Ensure ALL whitelabel data is included using helper function
2830 if (formData.indexOf('whitelabel') === -1) {
2831 var whitelabelData = collectWhitelabelFields();
2832 if (whitelabelData) {
2833 formData += '&' + whitelabelData;
2834 }
2835 }
2836
2837 // Get current tab from URL
2838 var urlParams = new URLSearchParams(window.location.search);
2839 var currentTab = urlParams.get('tab') || 'general';
2840
2841 formData += '&action=meta_sync_save_settings&active_tab=' + encodeURIComponent(currentTab);
2842
2843 $.ajax({
2844 url: metaSync.ajax_url,
2845 type: 'POST',
2846 data: formData,
2847 success: function (response) {
2848 if (response.success) {
2849 // Clear unsaved changes flag
2850 hasUnsavedChanges = false;
2851 updateUnsavedChangesIndicator();
2852
2853 // Clear any previous save notices (success, error, or warning)
2854 $('.metasync-save-notice').remove();
2855
2856 // Show temporary success indication in plugin area
2857 var noticeType, noticeMessage, noAutoDismiss = false;
2858 if (response.data && response.data.api_key_validated === true) {
2859 noticeType = 'notice-success';
2860 noticeMessage = $('<span/>').text('Settings saved successfully! API key verified & connected. Reloading\u2026').html();
2861 updateHeaderStatus(true, 'Synced', getPluginName() + ' connected');
2862 } else if (response.data && response.data.api_key_validated === false) {
2863 noticeType = 'notice-error';
2864 noticeMessage = $('<span/>').text(response.data.warning || 'The API key could not be verified. Your other settings were saved.').html();
2865 noAutoDismiss = true;
2866 // The server reverts the stored key to the last valid one on
2867 // verification failure, and the masked display is read-only, so
2868 // nothing in the DOM needs reverting here. The cleartext key is
2869 // never sent to the browser.
2870 } else if (response.data && response.data.api_key_removed === true) {
2871 // API key was cleared — show warning, update status, page will reload
2872 noticeType = 'notice-warning';
2873 noticeMessage = $('<span/>').text((response.data.warning || 'The API key has been removed.') + ' Reloading\u2026').html();
2874 updateHeaderStatus(false, 'Disconnected', getPluginName() + ' disconnected');
2875 } else if (response.data && response.data.warning) {
2876 // Network failure — key saved but unverified
2877 noticeType = 'notice-warning';
2878 noticeMessage = $('<span/>').text(response.data.warning).html();
2879 } else {
2880 noticeType = 'notice-success';
2881 noticeMessage = $('<span/>').text('Settings saved successfully!').html();
2882 }
2883 var successNotice = '<div class="notice ' + noticeType + ' is-dismissible metasync-save-notice" style="margin: 20px 0; padding: 12px;"><p><strong>' + noticeMessage + '</strong></p></div>';
2884
2885 // Insert between navigation menu and page content
2886 var $navWrapper = $('.metasync-nav-wrapper');
2887 if ($navWrapper.length > 0) {
2888 // Position after navigation menu but before first dashboard card or form
2889 $navWrapper.after(successNotice);
2890 } else {
2891 // Fallback: insert at top of settings page
2892 $('.metasync-dashboard-wrap').prepend(successNotice);
2893 }
2894
2895 // Scroll to the notice for better visibility
2896 $('html, body').animate({ scrollTop: 0 }, 'slow');
2897
2898 // Reload page when API key status changed so server-rendered
2899 // sections (One-Click Authentication, promo sidebar) update
2900 if (response.data && (response.data.api_key_validated === true || response.data.api_key_removed === true)) {
2901 setTimeout(function () {
2902 location.reload();
2903 }, 1500);
2904 return;
2905 }
2906
2907 // Auto-dismiss success/warning notices; keep errors visible
2908 if (!noAutoDismiss) {
2909 setTimeout(function () {
2910 $('.metasync-save-notice').fadeOut(300, function () {
2911 $(this).remove();
2912 });
2913 }, 3000);
2914 }
2915 } else {
2916 // Handle validation errors
2917 var errors = response.data && response.data.errors ? response.data.errors : [];
2918 var fieldErrors = (response.data && response.data.field_errors) ? response.data.field_errors : {};
2919
2920 // Build error notice using DOM methods to avoid XSS
2921 var $errorNotice = $('<div>').addClass('notice notice-error metasync-error-wrap').css({ margin: '20px', padding: '12px' });
2922 if (Array.isArray(errors)) {
2923 var $ul = $('<ul>');
2924 for (var i = 0; i < errors.length; i++) {
2925 $ul.append($('<li>').text(errors[i]));
2926 }
2927 $errorNotice.append($ul);
2928 } else {
2929 var message = 'An error occurred while saving settings.';
2930 if (response.data && response.data.message) {
2931 message = response.data.message;
2932 }
2933 $errorNotice.append($('<p>').text(message));
2934 }
2935
2936 // Insert error notice in plugin area
2937 var $navWrapper = $('.metasync-nav-wrapper');
2938 if ($navWrapper.length > 0) {
2939 $navWrapper.after($errorNotice);
2940 } else {
2941 // Fallback: insert at top of plugin content area
2942 $('.metasync-dashboard-wrap').prepend($errorNotice);
2943 }
2944
2945 // Field-specific feedback: show the server's per-field
2946 // error beside each rejected white-label branding input.
2947 clearBrandingFieldErrors();
2948 Object.keys(fieldErrors).forEach(function (settingKey) {
2949 showBrandingFieldError(settingKey, fieldErrors[settingKey]);
2950 });
2951
2952 // Scroll to the error message for better visibility
2953 $('html, body').animate({ scrollTop: 0 }, 'slow');
2954
2955 setTimeout(function () {
2956 $('.metasync-error-wrap').fadeOut(300, function () {
2957 $(this).remove();
2958 });
2959 }, 5000);
2960 }
2961 isSaving = false;
2962 },
2963 error: function (xhr, status, error) {
2964 // Handle AJAX error
2965 var errorMessage = 'There was an error saving the settings. Please try again.';
2966 if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) {
2967 errorMessage = xhr.responseJSON.data.message;
2968 }
2969
2970 var ajaxErrorNotice = '<div class="notice notice-error is-dismissible metasync-ajax-error" style="margin: 20px 0; padding: 12px;"><p><strong>❌ Error:</strong> ' + errorMessage + '</p></div>';
2971
2972 // Insert between navigation menu and page content
2973 var $navWrapper = $('.metasync-nav-wrapper');
2974 if ($navWrapper.length > 0) {
2975 // Position after navigation menu but before first dashboard card or form
2976 $navWrapper.after(ajaxErrorNotice);
2977 } else {
2978 // Fallback: insert at top of settings page
2979 $('.metasync-dashboard-wrap').prepend(ajaxErrorNotice);
2980 }
2981
2982 // Scroll to the error message for better visibility
2983 $('html, body').animate({ scrollTop: 0 }, 'slow');
2984
2985 setTimeout(function () {
2986 $('.metasync-ajax-error').fadeOut(300, function () {
2987 $(this).remove();
2988 });
2989 }, 5000);
2990
2991 isSaving = false;
2992 }
2993 });
2994 }
2995 };
2996
2997 // Enhanced beforeunload message (disabled when saving)
2998 var isSaving = false;
2999 $(window).on('beforeunload', function (e) {
3000 if (hasUnsavedChanges && !isSaving) {
3001 var message = '🔄 You have unsaved changes in MetaSync settings that will be lost if you leave this page.';
3002 e.returnValue = message; // For older browsers
3003 return message;
3004 }
3005 });
3006
3007 // Initialize the detection when page loads
3008 setTimeout(initializeUnsavedChangesDetection, 1000); // Small delay to ensure all elements are loaded
3009
3010 // Indexation Control form AJAX save functionality
3011 function initializeSeoControlsSaveHandler() {
3012 // Only initialize if we're on the Indexation Control page
3013 if ($('#metaSyncSeoControlsForm').length > 0) {
3014 // Override the saveChanges function for Indexation Control form
3015 window.saveChanges = function () {
3016 var $form = $('#metaSyncSeoControlsForm');
3017 if ($form.length > 0) {
3018 // Get form data and submit via AJAX
3019 var formData = $form.serialize();
3020 formData += '&action=meta_sync_save_seo_controls';
3021
3022 // Remove the notification immediately
3023 var $notification = $('.metasync-unsaved-notification');
3024 if ($notification.length > 0) {
3025 $notification.remove();
3026 }
3027
3028 // Clear previous messages
3029 $('#seo-controls-messages').empty();
3030
3031 $.ajax({
3032 url: ajaxurl || metaSync.ajax_url,
3033 type: 'POST',
3034 data: formData,
3035 dataType: 'json',
3036 success: function (response) {
3037 if (response.success) {
3038 // Show success message above Indexation Control section
3039 var $successNotice = $('<div>').addClass('notice notice-success is-dismissible').css('margin-bottom', '20px');
3040 var $successP = $('<p>');
3041 $successP.append($('<strong>').text('�
3042 Success! '));
3043 $successP[0].appendChild(document.createTextNode(response.data.message));
3044 $successNotice.append($successP);
3045 $('#seo-controls-messages').empty().append($successNotice);
3046
3047 // Clear unsaved changes state
3048 hasUnsavedChanges = false;
3049 if (typeof updateUnsavedChangesIndicator === 'function') {
3050 updateUnsavedChangesIndicator();
3051 }
3052
3053 // Update initial form data to current state after successful save
3054 var formId = $form.attr('id') || 'metaSyncSeoControlsForm';
3055 initialFormData[formId] = $form.serialize();
3056
3057 // Auto-hide success notice after 5 seconds
3058 setTimeout(function () {
3059 $('#seo-controls-messages .notice-success').fadeOut();
3060 }, 5000);
3061 } else {
3062 // Show error message above Indexation Control section
3063 var $errorNoticeCtrl = $('<div>').addClass('notice notice-error is-dismissible').css('margin-bottom', '20px');
3064 var $errorPCtrl = $('<p>');
3065 $errorPCtrl.append($('<strong>').text('❌ Error! '));
3066 $errorPCtrl[0].appendChild(document.createTextNode(response.data.message || 'Failed to save settings'));
3067 $errorNoticeCtrl.append($errorPCtrl);
3068 $('#seo-controls-messages').empty().append($errorNoticeCtrl);
3069 }
3070
3071 // Make dismiss buttons work
3072 $('#seo-controls-messages .notice-dismissible').each(function () {
3073 var $notice = $(this);
3074 if (!$notice.find('.notice-dismiss').length) {
3075 $notice.append('<button type="button" class="notice-dismiss"><span class="screen-reader-text">Dismiss this notice.</span></button>');
3076 }
3077 $notice.find('.notice-dismiss').on('click', function () {
3078 $notice.fadeOut();
3079 });
3080 });
3081 },
3082 error: function (xhr, status, error) {
3083 // Show error message above Indexation Control section
3084 $('#seo-controls-messages').html(
3085 '<div class="notice notice-error is-dismissible" style="margin-bottom: 20px;">' +
3086 '<p><strong>❌ Error!</strong> Network error occurred while saving. Please check your connection and try again.</p>' +
3087 '</div>'
3088 );
3089 console.error('AJAX Error:', error);
3090 console.error('XHR Status:', xhr.status);
3091 console.error('XHR Response:', xhr.responseText);
3092
3093 // Don't clear unsaved changes on network error
3094 // User may want to retry or fix the issue
3095 },
3096 complete: function () {
3097 // Re-enable save button if it was disabled
3098 $('.metasync-save-button').prop('disabled', false);
3099 }
3100 });
3101 } else {
3102 // Fallback to regular form submission
3103 $('form[method="post"]').first().submit();
3104 }
3105 };
3106 }
3107 }
3108
3109 // Initialize Indexation Control functionality
3110 initializeSeoControlsSaveHandler();
3111
3112 /**
3113 * Handle "All Roles" checkbox behavior for Content Genius sync
3114 * When "All Roles" is checked, uncheck all other role checkboxes
3115 * When any specific role is checked, uncheck "All Roles"
3116 *
3117 * @since 1.0.0
3118 * @returns {void}
3119 */
3120 function initializeContentGeniusRoleCheckboxes() {
3121 try {
3122 // Cache selectors for better performance
3123 var allRolesCheckbox = $('input[name="metasync_options[general][content_genius_sync_roles][]"][value="all"]');
3124 var roleCheckboxes = $('input[name="metasync_options[general][content_genius_sync_roles][]"]').not('[value="all"]');
3125
3126 // Exit early if elements don't exist
3127 if (allRolesCheckbox.length === 0 && roleCheckboxes.length === 0) {
3128 return;
3129 }
3130
3131 // When "All Roles" is checked, uncheck all other checkboxes
3132 if (allRolesCheckbox.length > 0) {
3133 allRolesCheckbox.off('change.metasyncRoles').on('change.metasyncRoles', function () {
3134 try {
3135 if ($(this).is(':checked')) {
3136 roleCheckboxes.prop('checked', false);
3137 // Visual feedback
3138 roleCheckboxes.closest('.metasync-role-option').removeClass('active');
3139 $(this).closest('.metasync-role-option-all').addClass('active');
3140 } else {
3141 $(this).closest('.metasync-role-option-all').removeClass('active');
3142 }
3143 } catch (e) {
3144 console.warn('MetaSync: Error handling "All Roles" checkbox change:', e);
3145 }
3146 });
3147 }
3148
3149 // When any specific role is checked, uncheck "All Roles"
3150 if (roleCheckboxes.length > 0) {
3151 roleCheckboxes.off('change.metasyncRoles').on('change.metasyncRoles', function () {
3152 try {
3153 if ($(this).is(':checked')) {
3154 allRolesCheckbox.prop('checked', false);
3155 allRolesCheckbox.closest('.metasync-role-option-all').removeClass('active');
3156 // Visual feedback
3157 $(this).closest('.metasync-role-option').addClass('active');
3158 } else {
3159 $(this).closest('.metasync-role-option').removeClass('active');
3160 }
3161 } catch (e) {
3162 console.warn('MetaSync: Error handling role checkbox change:', e);
3163 }
3164 });
3165 }
3166
3167 // Initialize active states on page load
3168 initializeRoleCheckboxStates();
3169
3170 } catch (error) {
3171 console.error('MetaSync: Failed to initialize Content Genius role checkboxes:', error);
3172 }
3173 }
3174
3175 /**
3176 * Initialize active states for role checkboxes on page load
3177 *
3178 * @since 1.0.0
3179 * @returns {void}
3180 */
3181 function initializeRoleCheckboxStates() {
3182 try {
3183 var allRolesCheckbox = $('input[name="metasync_options[general][content_genius_sync_roles][]"][value="all"]');
3184 var roleCheckboxes = $('input[name="metasync_options[general][content_genius_sync_roles][]"]').not('[value="all"]');
3185
3186 // Set active state for "All Roles" if checked
3187 if (allRolesCheckbox.is(':checked')) {
3188 allRolesCheckbox.closest('.metasync-role-option-all').addClass('active');
3189 }
3190
3191 // Set active state for checked roles
3192 roleCheckboxes.each(function () {
3193 if ($(this).is(':checked')) {
3194 $(this).closest('.metasync-role-option').addClass('active');
3195 }
3196 });
3197 } catch (e) {
3198 console.warn('MetaSync: Error initializing role checkbox states:', e);
3199 }
3200 }
3201
3202 // Initialize Content Genius role checkboxes with safety wrapper
3203 if (typeof $ !== 'undefined' && $.fn) {
3204 initializeContentGeniusRoleCheckboxes();
3205 } else {
3206 console.warn('MetaSync: jQuery not available for role checkbox initialization');
3207 }
3208
3209 // ========================================
3210 // SETTINGS ACCORDION FUNCTIONALITY
3211 // ========================================
3212
3213 /**
3214 * Initialize accordion sections with localStorage state persistence
3215 */
3216 function initSettingsAccordion() {
3217 var $accordionSections = $('.metasync-accordion-section');
3218
3219 if ($accordionSections.length === 0) {
3220 return; // No accordion on this page
3221 }
3222
3223 console.log('🎨 Initializing settings accordion with ' + $accordionSections.length + ' sections');
3224
3225 // Restore saved state from localStorage
3226 restoreAccordionState();
3227
3228 // Click handler for accordion headers
3229 $('.metasync-accordion-header').on('click', function (e) {
3230 toggleAccordionSection($(this));
3231 });
3232
3233 // Keyboard navigation (Enter/Space)
3234 $('.metasync-accordion-header').on('keydown', function (e) {
3235 if (e.key === 'Enter' || e.key === ' ') {
3236 e.preventDefault();
3237 toggleAccordionSection($(this));
3238 }
3239 });
3240 }
3241
3242 /**
3243 * Toggle accordion section open/closed
3244 * @param {jQuery} $header - The clicked header element
3245 */
3246 function toggleAccordionSection($header) {
3247 var $section = $header.closest('.metasync-accordion-section');
3248 var $content = $section.find('.metasync-accordion-content');
3249 var isOpen = $header.attr('aria-expanded') === 'true';
3250 var sectionKey = $section.data('section');
3251
3252 if (isOpen) {
3253 // Close section
3254 $header.attr('aria-expanded', 'false');
3255 $content.attr('data-state', 'closed');
3256 console.log('📁 Closed accordion section: ' + sectionKey);
3257 } else {
3258 // Open section
3259 $header.attr('aria-expanded', 'true');
3260 $content.attr('data-state', 'open');
3261 console.log('📂 Opened accordion section: ' + sectionKey);
3262 }
3263
3264 // Save state to localStorage
3265 saveAccordionState();
3266 }
3267
3268 /**
3269 * Save accordion state to localStorage
3270 */
3271 function saveAccordionState() {
3272 var state = {};
3273
3274 $('.metasync-accordion-section').each(function () {
3275 var sectionKey = $(this).data('section');
3276 var isOpen = $(this).find('.metasync-accordion-header').attr('aria-expanded') === 'true';
3277 state[sectionKey] = isOpen;
3278 });
3279
3280 try {
3281 localStorage.setItem('metasync_accordion_state', JSON.stringify(state));
3282 } catch (e) {
3283 console.warn('⚠️ Could not save accordion state to localStorage:', e);
3284 }
3285 }
3286
3287 /**
3288 * Restore accordion state from localStorage
3289 */
3290 function restoreAccordionState() {
3291 try {
3292 var savedState = localStorage.getItem('metasync_accordion_state');
3293 if (!savedState) {
3294 return; // No saved state, use defaults
3295 }
3296
3297 var state = JSON.parse(savedState);
3298
3299 $('.metasync-accordion-section').each(function () {
3300 var $section = $(this);
3301 var sectionKey = $section.data('section');
3302 var $header = $section.find('.metasync-accordion-header');
3303 var $content = $section.find('.metasync-accordion-content');
3304
3305 if (Object.prototype.hasOwnProperty.call(state, sectionKey)) {
3306 var shouldBeOpen = state[sectionKey];
3307 $header.attr('aria-expanded', shouldBeOpen ? 'true' : 'false');
3308 $content.attr('data-state', shouldBeOpen ? 'open' : 'closed');
3309 }
3310 });
3311
3312 console.log('💾 Restored accordion state from localStorage');
3313 } catch (e) {
3314 console.warn('⚠️ Could not restore accordion state:', e);
3315 }
3316 }
3317
3318 // Initialize accordion when DOM is ready
3319 initSettingsAccordion();
3320
3321 // ========================================
3322 // TOOLTIP SYSTEM
3323 // ========================================
3324
3325 /**
3326 * Initialize tooltip functionality
3327 */
3328 function initTooltipSystem() {
3329 console.log('🔍 Tooltip system initialization started');
3330
3331 var $tooltipTriggers = $('.metasync-tooltip-trigger');
3332 var currentTooltip = null;
3333
3334 console.log('🔍 Found ' + $tooltipTriggers.length + ' tooltip triggers');
3335
3336 if ($tooltipTriggers.length === 0) {
3337 console.warn('⚠️ No tooltip triggers found on this page');
3338 return; // No tooltips on this page
3339 }
3340
3341 console.log('💡 Initializing tooltip system with ' + $tooltipTriggers.length + ' tooltips');
3342
3343 // Click handler for tooltip triggers
3344 $tooltipTriggers.on('click', function (e) {
3345 e.preventDefault();
3346 e.stopPropagation();
3347
3348 var $trigger = $(this);
3349 var tooltipId = $trigger.data('tooltip-id');
3350 var $tooltip = $('#tooltip-' + tooltipId);
3351
3352 console.log('🖱️ Tooltip trigger clicked:', tooltipId);
3353 console.log('🎯 Tooltip element found:', $tooltip.length);
3354
3355 // Close other tooltips
3356 if (currentTooltip && currentTooltip[0] !== $tooltip[0]) {
3357 currentTooltip.removeClass('show');
3358 }
3359
3360 // Toggle current tooltip
3361 if ($tooltip.hasClass('show')) {
3362 $tooltip.removeClass('show');
3363 currentTooltip = null;
3364 } else {
3365 $tooltip.addClass('show');
3366 currentTooltip = $tooltip;
3367 positionTooltip($trigger, $tooltip);
3368 }
3369 });
3370
3371 // Hover handler (desktop only)
3372 var hideTimeout;
3373
3374 if (window.innerWidth > 768) {
3375 // Show tooltip on trigger hover
3376 $tooltipTriggers.on('mouseenter', function () {
3377 var $trigger = $(this);
3378 var tooltipId = $trigger.data('tooltip-id');
3379 var $tooltip = $('#tooltip-' + tooltipId);
3380
3381 // Clear any pending hide timeout
3382 clearTimeout(hideTimeout);
3383
3384 console.log('🖱️ Hover on trigger:', tooltipId);
3385
3386 // Close other tooltips
3387 if (currentTooltip && currentTooltip[0] !== $tooltip[0]) {
3388 currentTooltip.removeClass('show');
3389 }
3390
3391 $tooltip.addClass('show');
3392 currentTooltip = $tooltip;
3393 positionTooltip($trigger, $tooltip);
3394 });
3395
3396 // Start hide timer when leaving trigger
3397 $tooltipTriggers.on('mouseleave', function () {
3398 var tooltipId = $(this).data('tooltip-id');
3399 var $tooltip = $('#tooltip-' + tooltipId);
3400
3401 console.log('🖱️ Mouse left trigger:', tooltipId);
3402
3403 // Delay hiding to allow moving to tooltip
3404 hideTimeout = setTimeout(function () {
3405 // Only hide if not hovering tooltip
3406 if (!$tooltip.is(':hover')) {
3407 console.log('⏱️ Hiding tooltip:', tooltipId);
3408 $tooltip.removeClass('show');
3409 if (currentTooltip && currentTooltip[0] === $tooltip[0]) {
3410 currentTooltip = null;
3411 }
3412 } else {
3413 console.log('✋ Mouse is over tooltip, keeping visible');
3414 }
3415 }, 200);
3416 });
3417
3418 // Cancel hide when entering tooltip
3419 $('.metasync-tooltip').on('mouseenter', function () {
3420 console.log('🎯 Mouse entered tooltip');
3421 clearTimeout(hideTimeout);
3422 $(this).addClass('show');
3423 });
3424
3425 // Hide when leaving tooltip
3426 $('.metasync-tooltip').on('mouseleave', function () {
3427 console.log('🎯 Mouse left tooltip');
3428 var $tooltip = $(this);
3429
3430 hideTimeout = setTimeout(function () {
3431 var tooltipId = $tooltip.attr('id').replace('tooltip-', '');
3432 var $trigger = $('[data-tooltip-id="' + tooltipId + '"]');
3433
3434 // Only hide if not hovering trigger
3435 if (!$trigger.is(':hover')) {
3436 console.log('⏱️ Hiding tooltip from tooltip leave');
3437 $tooltip.removeClass('show');
3438 if (currentTooltip && currentTooltip[0] === $tooltip[0]) {
3439 currentTooltip = null;
3440 }
3441 } else {
3442 console.log('✋ Mouse is back on trigger, keeping visible');
3443 }
3444 }, 200);
3445 });
3446 }
3447
3448 // Close tooltip when clicking outside
3449 $(document).on('click', function (e) {
3450 if (!$(e.target).closest('.metasync-tooltip-trigger, .metasync-tooltip').length) {
3451 if (currentTooltip) {
3452 currentTooltip.removeClass('show');
3453 currentTooltip = null;
3454 }
3455 }
3456 });
3457
3458 // Keyboard accessibility - ESC to close
3459 $(document).on('keydown', function (e) {
3460 if (e.key === 'Escape' && currentTooltip) {
3461 currentTooltip.removeClass('show');
3462 currentTooltip = null;
3463 }
3464 });
3465
3466 // Reposition tooltips on window resize
3467 $(window).on('resize', function () {
3468 if (currentTooltip) {
3469 var tooltipId = currentTooltip.attr('id').replace('tooltip-', '');
3470 var $trigger = $('[data-tooltip-id="' + tooltipId + '"]');
3471 positionTooltip($trigger, currentTooltip);
3472 }
3473 });
3474 }
3475
3476 /**
3477 * Position tooltip relative to trigger
3478 * @param {jQuery} $trigger - The trigger button
3479 * @param {jQuery} $tooltip - The tooltip element
3480 */
3481 function positionTooltip($trigger, $tooltip) {
3482 // Mobile uses the CSS fixed bottom-sheet layout; clear any inline desktop
3483 // styles so the stylesheet takes over.
3484 if (window.innerWidth <= 768) {
3485 $tooltip.css({ position: '', left: '', top: '', right: '', bottom: '', transform: '', margin: '' });
3486 return;
3487 }
3488
3489 // Move the tooltip up to the plugin wrapper so it escapes the
3490 // overflow:hidden on accordion sections / cards that was clipping it.
3491 // The wrapper carries data-theme (so the --dashboard-* CSS vars still
3492 // resolve) and is position:relative (not transformed), so the
3493 // position:fixed below stays relative to the viewport. Verified in a
3494 // headless-Chrome harness before shipping.
3495 var portal = document.querySelector('.metasync-dashboard-wrap') || document.body;
3496 if ($tooltip[0].parentNode !== portal) {
3497 portal.appendChild($tooltip[0]);
3498 }
3499
3500 var margin = 12;
3501 var rect = $trigger[0].getBoundingClientRect();
3502 var vw = window.innerWidth;
3503 var vh = window.innerHeight;
3504 var tw = $tooltip.outerWidth();
3505 var th = $tooltip.outerHeight();
3506
3507 // Horizontal: prefer to the right of the icon, else flip left; clamp to viewport.
3508 var left = rect.right + margin;
3509 if (left + tw + margin > vw) { left = rect.left - margin - tw; }
3510 if (left < margin) { left = margin; }
3511 if (left + tw + margin > vw) { left = Math.max(margin, vw - tw - margin); }
3512
3513 // Vertical: center on the icon, then clamp so the top/bottom is never cut off.
3514 var top = rect.top + (rect.height / 2) - (th / 2);
3515 if (top < margin) { top = margin; }
3516 if (top + th + margin > vh) { top = Math.max(margin, vh - th - margin); }
3517
3518 $tooltip.find('.metasync-tooltip-arrow').css('display', 'none');
3519 // Place it, then play a small left-to-right slide-in. Position is
3520 // handled by left/top (fixed), so transform is free for the entrance
3521 // animation: start ~12px to the left and slide right into place.
3522 $tooltip.css({
3523 position: 'fixed',
3524 left: Math.round(left) + 'px',
3525 top: Math.round(top) + 'px',
3526 right: 'auto',
3527 bottom: 'auto',
3528 margin: '0',
3529 transition: 'opacity 0.2s ease',
3530 transform: 'translateX(-12px)'
3531 });
3532 // Force a reflow so the start position is committed before animating.
3533 void $tooltip[0].offsetWidth;
3534 $tooltip.css({
3535 transition: 'opacity 0.2s ease, transform 0.2s ease',
3536 transform: 'translateX(0)'
3537 });
3538 $tooltip.removeAttr('data-position');
3539 }
3540
3541 // Initialize tooltip system
3542 initTooltipSystem();
3543
3544 // PR3: Burst ping — 10 min polling when UNREGISTERED or KEY_PENDING. Stop after 5 failed attempts.
3545 (function () {
3546 if (typeof metaSync === 'undefined' || !metaSync.heartbeat_state) {
3547 return;
3548 }
3549 var state = metaSync.heartbeat_state;
3550 if (state !== 'UNREGISTERED' && state !== 'KEY_PENDING') {
3551 return;
3552 }
3553
3554 var INTERVAL_MS = 10 * 60 * 1000;
3555 var MAX_ATTEMPTS = 5;
3556 var attempts = 0;
3557 var lastState = state;
3558 var intervalId = null;
3559
3560 function stop() {
3561 if (intervalId) {
3562 clearInterval(intervalId);
3563 intervalId = null;
3564 }
3565 }
3566
3567 intervalId = setInterval(function () {
3568 $.post(metaSync.ajax_url, {
3569 action: 'metasync_burst_ping',
3570 nonce: metaSync.burst_ping_nonce
3571 })
3572 .done(function (res) {
3573 if (!res || !res.data) {
3574 attempts++;
3575 if (attempts >= MAX_ATTEMPTS) {
3576 stop();
3577 }
3578 return;
3579 }
3580 var data = res.data;
3581 var newState = data.state || lastState;
3582 if (newState !== lastState) {
3583 attempts = 0;
3584 lastState = newState;
3585 }
3586 if (data.heartbeat_confirmed || newState === 'CONNECTED') {
3587 stop();
3588 if (newState === 'CONNECTED' && typeof updateHeaderStatus === 'function') {
3589 var hasOttoUuid = typeof metaSync !== 'undefined' && metaSync.otto_pixel_uuid && metaSync.otto_pixel_uuid.trim() !== '';
3590 if (hasOttoUuid) {
3591 updateHeaderStatus(true, 'Synced', 'Heartbeat confirmed');
3592 } else {
3593 updateHeaderStatus(false, 'Warning', 'Connected but OTTO UUID is missing — deploys will not work. Please reconnect.');
3594 }
3595 }
3596 return;
3597 }
3598 attempts++;
3599 if (attempts >= MAX_ATTEMPTS) {
3600 stop();
3601 }
3602 })
3603 .fail(function () {
3604 attempts++;
3605 if (attempts >= MAX_ATTEMPTS) {
3606 stop();
3607 }
3608 });
3609 }, INTERVAL_MS);
3610 })();
3611
3612 });
3613
3614 })(jQuery);
3615