PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.15
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.15
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 2.6.4 2.6.3 2.5.23 All 138 releases
metasync / admin / js / metasync-admin.js

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

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