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

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

3,341 lines 113.1 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 || '[email protected]';
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 || '[email protected]';
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 // the header badge must reflect the confirmed heartbeat
1128 // round-trip (metaSync.is_connected), NOT mere API-key/UUID presence —
1129 // otherwise a revoked/invalid key still shows "Connected" because the
1130 // key string and UUID remain stored locally.
1131 // NOTE: wp_localize_script stringifies PHP booleans (true -> "1",
1132 // false -> ""), so normalise to a real boolean rather than ===.
1133 var heartbeatConnected = metaSync.is_connected === true
1134 || metaSync.is_connected === '1'
1135 || metaSync.is_connected === 1;
1136 if (isFullyConnected && heartbeatConnected) {
1137 updateHeaderStatus(true, 'Synced', getPluginName() + ' API key and ' + getOttoName() + ' UUID are configured');
1138 } else if (isFullyConnected) {
1139 // Key + UUID are configured but the heartbeat is not succeeding
1140 // (e.g. the key was revoked on the Search Atlas side).
1141 updateHeaderStatus(false, 'Not Connected', getPluginName() + ' API key is set but the heartbeat is not responding — the connection may be revoked or unreachable.');
1142 } else if (hasApiKey && !hasOttoUuid) {
1143 updateHeaderStatus(false, 'Warning', 'Connected but ' + getOttoName() + ' UUID is missing — deploys will not work. Please reconnect.');
1144 } else {
1145 updateHeaderStatus(false, 'Not Synced', 'Missing ' + getPluginName() + ' API key or ' + getOttoName() + ' UUID');
1146 }
1147 }
1148
1149 // Monitor API key field changes
1150 $('#searchatlas-api-key').on('input', updateConnectionStatus);
1151
1152 // Initial status update
1153 updateConnectionStatus();
1154
1155 // Initialize dashboard iframe functionality
1156 initializeDashboardIframe();
1157
1158 // Settings dropdown now handled by inline script in HTML
1159
1160 // Add debug function for connection status (accessible in console)
1161 window.debugConnectionStatus = function () {
1162 var apiKey = $('#searchatlas-api-key').val();
1163 var hasApiKey = apiKey.trim() !== '';
1164
1165 console.log('🔍 Connection Status Debug:', {
1166 searchatlas_api_key: hasApiKey ? (apiKey.substring(0, 8) + '...') : 'EMPTY',
1167 otto_pixel_uuid: metaSync.otto_pixel_uuid || 'NOT SET',
1168 connection_state: hasApiKey && metaSync.otto_pixel_uuid ? 'CONNECTED' :
1169 hasApiKey ? 'PARTIAL (Missing ' + getOttoName() + ' UUID)' : 'NOT CONNECTED',
1170 dashboard_tab_visible: hasApiKey && metaSync.otto_pixel_uuid ? 'YES' : 'NO',
1171 status_indicator_should_show: hasApiKey && metaSync.otto_pixel_uuid ? 'Synced' : 'Not Synced'
1172 });
1173 };
1174
1175
1176 });
1177
1178 // Settings dropdown is now handled by inline script in HTML for better reliability
1179
1180 /**
1181 * Initialize Dashboard Iframe functionality
1182 * Adds loading states and error handling for the embedded dashboard
1183 */
1184 function initializeDashboardIframe() {
1185 var $iframe = $('#metasync-dashboard-iframe');
1186
1187 if ($iframe.length === 0) {
1188 return; // No iframe on this page
1189 }
1190
1191 // Add loading indicator
1192 var $wrapper = $('.metasync-dashboard-iframe-wrapper');
1193 var loadingHTML = '<div class="metasync-dashboard-iframe-loading"><div class="spinner"></div><p>Loading dashboard...</p></div>';
1194 $wrapper.append(loadingHTML);
1195
1196 // Handle iframe load events
1197 $iframe.on('load', function () {
1198 $('.metasync-dashboard-iframe-loading').fadeOut(300);
1199
1200 // Log successful load
1201 console.log('Dashboard iframe loaded successfully');
1202 });
1203
1204 // Handle iframe error events
1205 $iframe.on('error', function () {
1206 $('.metasync-dashboard-iframe-loading').html(
1207 '<div style="text-align: center; color: #dc3232;">' +
1208 '<h3>❌ Dashboard Loading Error</h3>' +
1209 '<p>Unable to load the dashboard. Please check your connection.</p>' +
1210 '<button type="button" class="button button-primary" onclick="location.reload();">🔄 Reload Page</button>' +
1211 '</div>'
1212 );
1213
1214 console.error('Dashboard iframe failed to load');
1215 });
1216
1217 // Add keyboard shortcut for refreshing iframe
1218 $(document).on('keydown', function (e) {
1219 // Ctrl/Cmd + R on dashboard page refreshes iframe
1220 if ((e.ctrlKey || e.metaKey) && e.key === 'r' && $iframe.length > 0) {
1221 e.preventDefault();
1222 refreshDashboardIframe();
1223 }
1224 });
1225
1226 // Handle iframe resize for better mobile experience
1227 function adjustIframeHeight() {
1228 if (window.innerWidth <= 768) {
1229 $iframe.height(600);
1230 } else {
1231 $iframe.height(800);
1232 }
1233 }
1234
1235 // Adjust on window resize
1236 $(window).on('resize', adjustIframeHeight);
1237 adjustIframeHeight(); // Initial adjustment
1238 }
1239
1240 /**
1241 * Refresh Dashboard Iframe
1242 * Reloads the iframe content with loading indicator
1243 */
1244 function refreshDashboardIframe() {
1245 var $iframe = $('#metasync-dashboard-iframe');
1246 var $wrapper = $('.metasync-dashboard-iframe-wrapper');
1247
1248 if ($iframe.length === 0) {
1249 return;
1250 }
1251
1252 // Show loading indicator
1253 $('.metasync-dashboard-iframe-loading').remove();
1254 var loadingHTML = '<div class="metasync-dashboard-iframe-loading"><div class="spinner"></div><p>Refreshing dashboard...</p></div>';
1255 $wrapper.append(loadingHTML);
1256
1257 // Refresh iframe
1258 var currentSrc = $iframe.attr('src');
1259 $iframe.attr('src', '');
1260 setTimeout(function () {
1261 $iframe.attr('src', currentSrc);
1262 }, 100);
1263
1264 console.log('Dashboard iframe refresh initiated');
1265 }
1266
1267 /**
1268 * Handle Search Atlas Authentication Reset.
1269 * Shows confirmation dialog and clears the Search Atlas API key and Otto UUID.
1270 */
1271 function handleSearchAtlasResetAuth() {
1272 // Show confirmation dialog
1273 var confirmed = confirm(
1274 '⚠️ Disconnect ' + getPluginName() + ' Account\n\n' +
1275 'This will:\n' +
1276 '• Remove your ' + getPluginName() + ' API key\n' +
1277 '• Clear all authentication tokens\n' +
1278 '• Reset connection timestamps\n' +
1279 '• Clear cached authentication data\n\n' +
1280 'You will need to re-authenticate to use ' + getPluginName() + ' features.\n\n' +
1281 'Are you sure you want to continue?'
1282 );
1283
1284 if (!confirmed) {
1285 return;
1286 }
1287
1288 var $resetButton = $('#reset-searchatlas-auth');
1289 var $connectButton = $('#connect-searchatlas-btn');
1290 var $apiKeyField = $('#searchatlas-api-key');
1291
1292 // Show loading state (prevent dashboard.js conflicts)
1293 $resetButton.prop('disabled', true)
1294 .addClass('no-loading') // Prevent dashboard.js interference
1295 .removeClass('dashboard-loading')
1296 .html('<span class="metasync-sa-connect-loading"></span> Disconnecting...');
1297
1298 // Show status message
1299 showConnectInfo('🔄 Disconnecting', 'Clearing your ' + getPluginName() + ' authentication data...');
1300
1301 // Make AJAX call to reset authentication
1302 $.ajax({
1303 url: ajaxurl,
1304 type: 'POST',
1305 data: {
1306 action: 'metasync_reset_authentication',
1307 nonce: metaSync.reset_auth_nonce
1308 },
1309 success: function (response) {
1310 if (response.success) {
1311 // Clear the API key field
1312 $apiKeyField.val('');
1313
1314 // Update button states
1315 $connectButton.html('🔗 Connect to ' + getPluginName());
1316 $resetButton.remove(); // Remove reset button since no longer connected
1317
1318 // Show clean success message without duplicate connect functionality
1319 showConnectSuccess('�
1320 Account Disconnected',
1321 'Your ' + getPluginName() + ' authentication has been completely reset. All authentication data has been cleared.',
1322 [{
1323 text: '📄 View What Was Cleared',
1324 action: function () {
1325 showClearedDataDetails(response.data.cleared_data);
1326 },
1327 primary: true
1328 }, {
1329 text: '�
1330 Got it',
1331 action: function () {
1332 hideConnectStatus();
1333 }
1334 }]
1335 );
1336
1337 // Update page elements to reflect disconnected state
1338 updateUIForDisconnectedState();
1339
1340 } else {
1341 showConnectError('❌ Reset Failed',
1342 response.data.message || 'Failed to reset authentication',
1343 [{
1344 text: '🔄 Try Again',
1345 action: function () {
1346 handleSearchAtlasResetAuth();
1347 }
1348 }, {
1349 text: '💬 Contact Support',
1350 action: function () {
1351 var supportEmail = metaSync.support_email || '[email protected]';
1352 window.open('mailto:' + supportEmail + '?subject=Authentication Reset Failed', '_blank');
1353 }
1354 }]
1355 );
1356 }
1357 },
1358 error: function (xhr, status, error) {
1359 showConnectError('🌐 Network Error',
1360 'A network error occurred while trying to reset authentication.',
1361 [{
1362 text: '🔄 Try Again',
1363 action: function () {
1364 handleSearchAtlasResetAuth();
1365 }
1366 }]
1367 );
1368 },
1369 complete: function () {
1370 // Reset button state
1371 $resetButton.prop('disabled', false)
1372 .removeClass('dashboard-loading no-loading')
1373 .html('🔓 Disconnect Account');
1374 }
1375 });
1376 }
1377
1378 /**
1379 * Show details of what data was cleared during reset
1380 */
1381 function showClearedDataDetails(clearedData) {
1382 var details = '<div style="max-width: 500px;"><h3>🗑️ Data Cleared</h3><ul style="text-align: left; margin: 15px 0;">';
1383
1384 for (var key in clearedData) {
1385 var displayName = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
1386 details += '<li><strong>' + displayName + ':</strong> ' + clearedData[key] + '</li>';
1387 }
1388
1389 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>';
1390
1391 showConnectInfo('📋 Reset Details', details, [{
1392 text: '�
1393 Got it',
1394 action: function () {
1395 hideConnectStatus();
1396 // Highlight the main connect button briefly to guide user attention
1397 var $mainButton = $('#connect-searchatlas-btn');
1398 if ($mainButton.length > 0) {
1399 $mainButton.addClass('metasync-pulse');
1400 setTimeout(function () {
1401 $mainButton.removeClass('metasync-pulse');
1402 }, 2000);
1403 }
1404 },
1405 primary: true
1406 }]);
1407 }
1408
1409 /**
1410 * Update UI elements when account is disconnected
1411 */
1412 function updateUIForDisconnectedState() {
1413 // a cached manual-sync cooldown must not survive disconnect —
1414 // it keeps the Sync Now button locked after the user reconnects even
1415 // though the server-side cooldown stamp was cleared with the auth data.
1416 localStorage.removeItem('metasync_manual_sync_throttle_expires');
1417 refreshManualSyncCooldownUI();
1418 $('#sendAuthToken').prop('disabled', false).removeClass('is-throttled').text('🔄 Sync Now');
1419
1420 // Update API key field placeholder
1421 $('#searchatlas-api-key').attr('placeholder', 'Your API key will appear here after authentication');
1422
1423 // �
1424 Clear OTTO Pixel UUID field
1425 $('input[name="metasync_options[general][otto_pixel_uuid]"]').val('');
1426
1427 // Note: OTTO SSR is always enabled by default, no checkbox to uncheck
1428
1429 // Remove synced indicator from API key field
1430 $('.metasync-sa-connect-container').find('span:contains("✓ Synced")').remove();
1431 $('label[for="searchatlas-api-key"]').find('span').remove(); // Remove any status spans
1432
1433 // Update header status indicator to "Not Synced"
1434 updateHeaderStatus(false, 'Not Synced', 'Missing ' + getPluginName() + ' API key or ' + getOttoName() + ' UUID');
1435
1436 // Update metaSync object for JavaScript state tracking
1437 if (typeof metaSync !== 'undefined') {
1438 metaSync.searchatlas_api_key = false;
1439 metaSync.otto_pixel_uuid = '';
1440 metaSync.is_connected = false;
1441 }
1442
1443 // Update descriptions to reflect disconnected state
1444 $('.metasync-sa-connect-description').html(
1445 'Connect your ' + getPluginName() + ' account with one click. This will automatically configure your API key below and enable all plugin features.'
1446 );
1447
1448 // Clear timestamp display if it exists
1449 $('#sendAuthTokenTimestamp').fadeOut(300);
1450
1451 // Header status already updated above - no need for additional connection status call
1452
1453 console.log('🔄 UI updated to reflect disconnected state - cleared API key, OTTO UUID, and OTTO enable checkbox');
1454
1455 // Show clean success message without duplicate connect button
1456 setTimeout(function () {
1457 showConnectSuccess('�
1458 Account Disconnected',
1459 'Your ' + getPluginName() + ' authentication has been completely reset. Use the "Connect to ' + getPluginName() + '" button above to reconnect.',
1460 [{
1461 text: '�
1462 Got it',
1463 action: function () {
1464 hideConnectStatus();
1465 },
1466 primary: true
1467 }]
1468 );
1469 }, 500); // Shorter delay since no action is needed
1470 }
1471
1472 /**
1473 * Update UI elements when account is connected/authenticated
1474 * Complementary function to updateUIForDisconnectedState()
1475 */
1476 function updateUIForConnectedState(apiKey, ottoPixelUuid) {
1477 // Update API key field
1478 if (apiKey) {
1479 $('#searchatlas-api-key').val(apiKey);
1480 }
1481
1482 // �
1483 Update OTTO Pixel UUID field
1484 if (ottoPixelUuid) {
1485 $('input[name="metasync_options[general][otto_pixel_uuid]"]').val(ottoPixelUuid);
1486 }
1487
1488 // Note: OTTO SSR is always enabled by default, no checkbox needed
1489
1490 // Update header status indicator based on UUID presence
1491 if (ottoPixelUuid) {
1492 updateHeaderStatus(true, 'Synced', 'Authentication completed - heartbeat sync will be validated on next page load');
1493 } else {
1494 updateHeaderStatus(false, 'Warning', 'Connected but ' + getOttoName() + ' UUID is missing — deploys will not work. Please reconnect.');
1495 }
1496
1497 // Update metaSync object for JavaScript state tracking
1498 if (typeof metaSync !== 'undefined') {
1499 metaSync.searchatlas_api_key = true;
1500 metaSync.is_connected = true;
1501 if (ottoPixelUuid) {
1502 metaSync.otto_pixel_uuid = ottoPixelUuid;
1503 }
1504 }
1505
1506 // Update descriptions to reflect connected state
1507 $('.metasync-sa-connect-description').html(
1508 'Your ' + getPluginName() + ' account is connected and synced successfully. All plugin features are now enabled.'
1509 );
1510
1511 console.log('�
1512 UI updated to reflect connected state - API key set, OTTO UUID set (SSR always enabled)');
1513 }
1514
1515 function addClassTableRowLocalSEO() {
1516 if (document.getElementsByClassName('form-table') && document.getElementById('local_seo_person_organization')) {
1517 const myElement = document.getElementsByTagName('tr');
1518
1519 for (let i = 0; i < myElement.length; i++) {
1520 myElement[i].classList.add('metasync-seo-' + (i + 10));
1521 }
1522 }
1523 }
1524
1525 function addClassTableRowSiteInfo() {
1526 if (document.getElementsByClassName('form-table') && document.getElementById('site_info_type')) {
1527 const myElement = document.getElementsByTagName('tr');
1528
1529 for (let i = 0; i < myElement.length; i++) {
1530 myElement[i].classList.add('metasync-site-info-' + (i + 10));
1531 }
1532 }
1533 }
1534
1535 function uploadMedia(title, text, input, src, closeBtn) {
1536
1537 var mediaUploader;
1538
1539 // If the uploader object has already been created, reopen the dialog
1540 if (mediaUploader) {
1541 mediaUploader.open();
1542 return;
1543 }
1544 // Extend the wp.media object
1545 mediaUploader = wp.media.frames.file_frame = wp.media({
1546 title: title,
1547 button: {
1548 text: text
1549 }, multiple: false
1550 });
1551
1552 // When a file is selected, grab the URL and set it as the text field's value
1553 mediaUploader.on('select', function () {
1554 var attachment = mediaUploader.state().get('selection').first().toJSON();
1555 jQuery('#' + input).val(attachment.id);
1556 jQuery('#' + src).attr('src', attachment.url);
1557 jQuery('#' + src).attr('width', 300);
1558 jQuery('#' + closeBtn).attr('type', 'button');
1559 jQuery('#' + src).show();
1560 jQuery('#' + closeBtn).show();
1561 });
1562 // Open the uploader dialog
1563 mediaUploader.open();
1564 }
1565
1566 function getLocalSeoOnLoadPage() {
1567 if (document.getElementsByClassName('form-table') && document.getElementById('local_seo_person_organization')) {
1568 var $type = $('#local_seo_person_organization').val();
1569 const classes = ['17', '18', '19', '20', '21', '24', '25'];
1570 if ($type === 'Person') {
1571 for (let i = 0; i < classes.length; i++) {
1572 $('.metasync-seo-' + classes[i]).hide();
1573 }
1574 $('.metasync-seo-15').show();
1575 } else {
1576 for (let i = 0; i < classes.length; i++) {
1577 $('.metasync-seo-' + classes[i]).show();
1578 }
1579 $('.metasync-seo-15').hide();
1580 }
1581 }
1582 }
1583
1584 function siteInfoOnLoadPage() {
1585 if (document.getElementsByClassName('form-table') && document.getElementById('site_info_type')) {
1586 var $type = $('#site_info_type').val();
1587 const classes = ['18', '19'];
1588 if ($type === 'blog' || $type === 'portfolio' || $type === 'otherpersonal') {
1589 for (let i = 0; i < classes.length; i++) {
1590 $('.metasync-site-info-' + classes[i]).hide();
1591 }
1592 } else {
1593 for (let i = 0; i < classes.length; i++) {
1594 $('.metasync-site-info-' + classes[i]).show();
1595 }
1596 }
1597 }
1598 }
1599
1600 function deleteTime() {
1601 $(this).parent().remove();
1602 }
1603
1604 function hideElementById(id) {
1605 if ($('#' + id)) {
1606 $('#' + id).hide();
1607 }
1608 }
1609
1610 function removeValueById(id) {
1611 if ($('#' + id)) {
1612 $('#' + id).val('');
1613 }
1614 }
1615
1616 $(function () {
1617 $('#addNewTime').on('click', function () {
1618 $('#daysTime').append(
1619 '<li>' +
1620 '<select name="metasync_options[localseo][days][]">' +
1621 '<option value="Monday">Monday</option>' +
1622 '<option value="Tuseday">Tuseday</option>' +
1623 '<option value="Wednesday">Wednesday</option>' +
1624 '<option value="Thursday">Thursday</option>' +
1625 '<option value="Friday">Friday</option>' +
1626 '<option value="Saturday">Saturday</option>' +
1627 '<option value="Sunday">Sunday</option>' +
1628 '</select>' +
1629 '<input type="text" name="metasync_options[localseo][times][]">' +
1630 '<button id="timeDelete">Delete</button>' +
1631 '</li>');
1632 return;
1633 });
1634 $(document).on('click', '#timeDelete', deleteTime);
1635 });
1636
1637 function deleteNumber() {
1638 $(this).parent().remove();
1639 }
1640
1641 $(function () {
1642 $('#addNewNumber').on('click', function () {
1643 $('#phone-numbers').append(
1644 '<li>' +
1645 '<select name="metasync_options[localseo][phonetype][]">' +
1646 '<option value="Customer Service">Customer Service</option>' +
1647 '<option value="Technical Support">Technical Support</option>' +
1648 '<option value="Billing Support">Billing Support</option>' +
1649 '<option value="Bill Payment">Bill Payment</option>' +
1650 '<option value="Sales">Sales</option>' +
1651 '<option value="Reservations">Reservations</option>' +
1652 '<option value="Credit Card Support">Credit Card Support</option>' +
1653 '<option value="Emergency">Emergency</option>' +
1654 '<option value="Baggage Tracking">Baggage Tracking</option>' +
1655 '<option value="Roadside Assistance">Roadside Assistance</option>' +
1656 '<option value="Package Tracking">Package Tracking</option>' +
1657 '</select>' +
1658 '<input type="text" name="metasync_options[localseo][phonenumber][]">' +
1659 '<button id="number-delete">Delete</button>' +
1660 '</li>');
1661 return;
1662 });
1663 $(document).on('click', '#number-delete', deleteNumber);
1664 });
1665
1666 function deleteSourceUrl() {
1667 $(this).parent().remove();
1668 }
1669 $(function () {
1670 $('#addNewSourceUrl').on('click', function () {
1671 $('#source_urls').append(
1672 '<li>' +
1673 '<input type="text" class="regular-text" name="source_url[]">' +
1674 '<select name="search_type[]">' +
1675 '<option value="exact">Exact</option>' +
1676 '<option value="contain">Contain</option>' +
1677 '<option value="start">Start With</option>' +
1678 '<option value="end">End With</option>' +
1679 '</select>' +
1680 '<button id="source_url_delete">Remove</button>' +
1681 '</li>');
1682 return;
1683 });
1684 $(document).on('click', '#source_url_delete', deleteSourceUrl);
1685 });
1686
1687 $(function () {
1688
1689 setToken();
1690
1691 $('body').on('click', '#wp_metasync_sync', function (e) {
1692 e.preventDefault();
1693 metasync_syncPostsAndPages();
1694 });
1695 $('body').on('click', '#metasync_settings_genkey_btn', function () {
1696 $('#apikey').val(metasyncGenerateAPIKey());
1697 });
1698 $('body').on('click', '#lgloginbtn', function () {
1699 // Hide any existing error messages first
1700 $('#lgerror').addClass('hidden').hide();
1701
1702 if ($('#lgusername').val() === '' || $('#lgpassword').val() === '') {
1703 $('.input.lguser').toggleClass('hidden');
1704 } else {
1705 metasyncLGLogin($('#lgusername').val(), $('#lgpassword').val());
1706 }
1707 });
1708
1709 // Enhanced SSO Connect button event handler
1710 // Aggressive event binding that overrides dashboard.js interference
1711 $('body').off('click', '#connect-searchatlas-btn').on('click', '#connect-searchatlas-btn', function (e) {
1712
1713 // Aggressively prevent dashboard interference
1714 preventDashboardInterference($(this));
1715
1716 // Only proceed if button is not in SSO process
1717 if (!$(this).hasClass('connecting') && !$(this).hasClass('authenticating')) {
1718 e.preventDefault();
1719 e.stopPropagation();
1720
1721
1722 handleSearchAtlasConnect();
1723 } else {
1724 }
1725 });
1726
1727 // Also add a direct event listener as backup
1728 setTimeout(function () {
1729 var $btn = $('#connect-searchatlas-btn');
1730 if ($btn.length > 0) {
1731 $btn[0].addEventListener('click', function (e) {
1732 e.preventDefault();
1733 e.stopPropagation();
1734
1735 // Force enable the button and clean classes
1736 preventDashboardInterference($(this));
1737
1738 if (!$(this).hasClass('connecting') && !$(this).hasClass('authenticating')) {
1739 handleSearchAtlasConnect();
1740 }
1741 }, true); // Use capture phase to get event before other handlers
1742 }
1743 }, 500);
1744
1745 // Monitor button state changes and fix interference
1746 setTimeout(function () {
1747 var $btn = $('#connect-searchatlas-btn');
1748 if ($btn.length > 0) {
1749 // Store original button state for restoration
1750 var buttonState = {
1751 disabled: $btn.prop('disabled'),
1752 style: $btn.attr('style'),
1753 pointerEvents: $btn.css('pointer-events'),
1754 zIndex: $btn.css('z-index'),
1755 position: $btn.css('position'),
1756 classes: $btn.attr('class')
1757 };
1758
1759 // Monitor for unwanted changes to the button
1760 var observer = new MutationObserver(function (mutations) {
1761 mutations.forEach(function (mutation) {
1762 if (mutation.type === 'attributes') {
1763 // Monitor for unwanted attribute changes
1764
1765 // Fix dashboard interference automatically
1766 if (mutation.attributeName === 'class' && $btn.hasClass('dashboard-loading')) {
1767 preventDashboardInterference($btn);
1768 }
1769
1770 if (mutation.attributeName === 'disabled' && $btn.prop('disabled') && !$btn.hasClass('connecting')) {
1771 preventDashboardInterference($btn);
1772 }
1773 }
1774 });
1775 });
1776
1777 observer.observe($btn[0], {
1778 attributes: true,
1779 attributeOldValue: true,
1780 attributeFilter: ['class', 'disabled', 'style']
1781 });
1782 }
1783 }, 1000);
1784
1785 // SSO Reset button event handler
1786 $('body').on('click', '#reset-searchatlas-auth', function (e) {
1787 e.preventDefault();
1788 handleSearchAtlasResetAuth();
1789 });
1790
1791 $('body').on('click', '#local_seo_logo_close_btn', function () {
1792 removeValueById('local_seo_logo');
1793 hideElementById('local_seo_business_logo');
1794 hideElementById('local_seo_logo_close_btn');
1795 });
1796
1797 $('body').on('click', '#site_google_logo_close_btn', function () {
1798 removeValueById('site_google_logo');
1799 hideElementById('site_google_logo_img');
1800 hideElementById('site_google_logo_close_btn');
1801 });
1802
1803 $('body').on('click', '#site_social_image_close_btn', function () {
1804 removeValueById('site_social_share_image');
1805 hideElementById('site_social_share_img');
1806 hideElementById('site_social_image_close_btn');
1807 });
1808
1809 $('body').on('click', '#logo_upload_button', function () {
1810 uploadMedia('Logo', 'Add', 'local_seo_logo', 'local_seo_business_logo', 'local_seo_logo_close_btn');
1811 });
1812
1813 $('body').on('click', '#google_logo_btn', function () {
1814 uploadMedia('Site Google Logo', 'Add', 'site_google_logo', 'site_google_logo_img', 'site_google_logo_close_btn');
1815 });
1816
1817 $('body').on('click', '#social_share_image_btn', function () {
1818 uploadMedia('Site Social Share Image', 'Add', 'site_social_share_image', 'site_social_share_img', 'site_social_image_close_btn');
1819 });
1820
1821 $('body').on('click', '#robots_common1', function () {
1822 $('#robots_common1').prop('checked', true);
1823 $('#robots_common2').prop('checked', false);
1824 });
1825
1826 $('body').on('click', '#robots_common2', function () {
1827 $('#robots_common1').prop('checked', false);
1828 $('#robots_common2').prop('checked', true);
1829 });
1830
1831 addClassTableRowLocalSEO();
1832
1833 addClassTableRowSiteInfo();
1834
1835 getLocalSeoOnLoadPage();
1836
1837 siteInfoOnLoadPage();
1838
1839 $('#local_seo_person_organization').change(function () {
1840 const classes = ['17', '18', '19', '20', '21', '24', '25'];
1841 if (this.value === 'Person') {
1842 for (let i = 0; i < classes.length; i++) {
1843 $('.metasync-seo-' + classes[i]).hide();
1844 }
1845 $('.metasync-seo-15').show();
1846 } else {
1847 for (let i = 0; i < classes.length; i++) {
1848 $('.metasync-seo-' + classes[i]).show();
1849 }
1850 $('.metasync-seo-15').hide();
1851 }
1852 });
1853
1854 $('#site_info_type').change(function () {
1855 const classes = ['18', '19'];
1856 if (this.value === 'blog' || this.value === 'portfolio' || this.value === 'otherpersonal') {
1857 for (let i = 0; i < classes.length; i++) {
1858 $('.metasync-site-info-' + classes[i]).hide();
1859 }
1860 } else {
1861 for (let i = 0; i < classes.length; i++) {
1862 $('.metasync-site-info-' + classes[i]).show();
1863 }
1864 }
1865 });
1866
1867 $('#metasync-giapi-response').hide();
1868
1869 $('body').on('click', '#metasync-btn-send', function () {
1870
1871 var url = $('#metasync-giapi-url');
1872 var action = $('input[type="radio"]:checked');
1873 var response = $('#metasync-giapi-response');
1874
1875 var urls = url.val().split('\n').filter(Boolean);
1876
1877 var urls_str = urls[0];
1878 var is_bulk = false;
1879 if (urls.length > 1) {
1880 urls_str = urls;
1881 is_bulk = true;
1882 }
1883
1884 jQuery.ajax({
1885 method: 'POST',
1886 url: 'admin-ajax.php',
1887 data: {
1888 action: 'metasync_send_giapi',
1889 nonce: metaSync.nonce,
1890 metasync_giapi_url: url.val(),
1891 metasync_giapi_action: action.val()
1892 }
1893 })
1894 .always(function (info) {
1895
1896 response.show();
1897
1898 $('.result-action').html('<strong>' + action.val() + '</strong>' + ' <br> ' + urls_str);
1899
1900 if (!is_bulk) {
1901 if (typeof info.error !== 'undefined') {
1902 $('.result-status-code').text(info.error.code).siblings('.result-message').text(info.error.message);
1903 } else {
1904 var d = new Date();
1905 $('.result-status-code').text('Success').siblings('.result-message').text(d.toString());
1906 }
1907 } else {
1908 $('.result-status-code').text('Success').siblings('.result-message').text('Success');
1909 if (typeof info.error !== 'undefined') {
1910 $('.result-status-code').text(info.error.code).siblings('.result-message').text(info.error.message);
1911 } else {
1912 $.each(info, function (index, val) {
1913
1914 if (typeof val.error !== 'undefined') {
1915 var error_code = '';
1916 if (typeof val.error.code !== 'undefined') {
1917 error_code = val.error.code;
1918 }
1919 var error_message = '';
1920 if (typeof val.error.message !== 'undefined') {
1921 error_message = val.error.message;
1922 }
1923 $('.result-status-code').text(error_code).siblings('.result-message').text(val.error.message);
1924 }
1925 });
1926 }
1927 }
1928 });
1929 });
1930
1931 $('body').on('click', '#cancel-redirection', function () {
1932 $('#add-redirection-form').hide();
1933 $('#add-redirection').focus();
1934 });
1935
1936 $('body').on('click', '.redirect_type', function () {
1937 if ($(this).val() === '410' || $(this).val() === '451') {
1938 $('#destination_url').val('');
1939 $('#destination').hide();
1940 } else {
1941 $('#destination').show();
1942 }
1943 });
1944
1945 if ($('#post_redirection').is(':checked')) {
1946 $('.hide').fadeIn('slow');
1947 }
1948 $('body').on('change', '#post_redirection', function () {
1949 if (this.checked) {
1950 $('.hide').fadeIn('slow');
1951 } else {
1952 $('.hide').fadeOut('slow');
1953 }
1954 });
1955
1956 $(document).ready(function () {
1957 if ($('#post_redirection').is(':checked')
1958 && ($('#post_redirection_type').val() === '410'
1959 || $('#post_redirection_type').val() === '451')) {
1960 $('#post_redirect_url').hide();
1961 }
1962 });
1963
1964 $('#post_redirection_type').change(function () {
1965 if ($('#post_redirection').is(':checked')
1966 && ($(this).val() === '410'
1967 || $(this).val() === '451')) {
1968 $('#post_redirect_url').hide();
1969 } else {
1970 $('#post_redirect_url').show();
1971 }
1972 });
1973
1974 });
1975
1976 $(function () {
1977 var psconsole = $('#error-code-box');
1978 if (psconsole.length) {
1979 psconsole.scrollTop(psconsole[0].scrollHeight - psconsole.height());
1980 }
1981 });
1982
1983 $(function () {
1984 $('#copy-clipboard-btn').on('click', function () {
1985 var hiddenInput = document.createElement('input');
1986 hiddenInput.setAttribute('value', document.getElementById('error-code-box').value);
1987 document.body.appendChild(hiddenInput);
1988 hiddenInput.select();
1989 document.execCommand('copy');
1990 document.body.removeChild(hiddenInput);
1991 });
1992 });
1993
1994 function dateFormat() {
1995 var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
1996 var m = new Date();
1997 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');
1998 }
1999
2000 function sendCustomerParams(is_hb = false) {
2001 // Only show alerts for manual sync (not heartbeat)
2002 const showAlerts = !is_hb;
2003
2004 // Remove any existing sync-related notices
2005 if (showAlerts) {
2006 $('.metasync-sync-notice, .metasync-sync-error').remove();
2007 }
2008
2009 // DEBUG: Log request parameters
2010 const requestData = {
2011 action: 'metasync_send_customer_params',
2012 nonce: metaSync.nonce,
2013 is_heart_beat : is_hb
2014 };
2015
2016 jQuery.ajax({
2017 type: 'post',
2018 url: 'admin-ajax.php',
2019 data: requestData,
2020 beforeSend: function () {
2021 if (showAlerts) {
2022 // Show loading state on button
2023 $('#sendAuthToken').prop('disabled', true).html('🔄 Syncing...');
2024 }
2025 },
2026 success: function (response) {
2027 // Reset button state
2028 if (showAlerts) {
2029 $('#sendAuthToken').prop('disabled', false).html('🔄 Sync Now');
2030 }
2031
2032 if ($('#searchatlas-api-key') && $('#searchatlas-api-key').val() === '') {
2033 if (!is_hb) {
2034 $('#sendAuthTokenTimestamp').html('Please save your ' + getPluginName() + ' API key');
2035 $('#sendAuthTokenTimestamp').css({ color: 'red' });
2036
2037 // Remove stale PHP-rendered "✓ Synced" label.
2038 $('label[for="searchatlas-api-key"]').find('span:contains("Synced")').remove();
2039
2040 updateHeaderStatus(false, 'Not Synced', 'Not Synced - API key required');
2041 }
2042
2043 if (showAlerts) {
2044 showSyncError('⚠️ API Key Required', 'Please save your ' + getPluginName() + ' API key in the settings above before syncing.');
2045 }
2046
2047 } else if (response && response.throttled) {
2048 // Compute expiry locally from a duration (remaining_seconds) so server/client
2049 // clock drift cannot lock the user out or let them bypass the throttle.
2050 var remainingSeconds = (typeof response.remaining_seconds === 'number')
2051 ? response.remaining_seconds
2052 : ((response.remaining_minutes || 5) * 60);
2053 var expiryMs = Date.now() + (remainingSeconds * 1000);
2054 var remainingMinutes = Math.max(1, Math.ceil(remainingSeconds / 60));
2055
2056 if (showAlerts) {
2057 localStorage.setItem('metasync_manual_sync_throttle_expires', String(expiryMs));
2058 refreshManualSyncCooldownUI();
2059 }
2060
2061 // Throttle is transient — don't change the connection badge.
2062 // The toast notification already informs the user.
2063
2064 if (showAlerts) {
2065 showSyncError('⏰ Request Throttled', response.message || 'Please wait ' + remainingMinutes + ' minutes before making another sync request.');
2066 }
2067
2068 } else if (response && response.detail) {
2069 if (!is_hb) {
2070 $('#sendAuthTokenTimestamp').html('Please provide a valid ' + getPluginName() + ' API key');
2071 $('#sendAuthTokenTimestamp').css({ color: 'red' });
2072
2073 // Remove stale PHP-rendered "✓ Synced" label — key is invalid on the server.
2074 $('label[for="searchatlas-api-key"]').find('span:contains("Synced")').remove();
2075 }
2076
2077 // No is_hb guard here — genuinely invalid keys should always flip the badge
2078 updateHeaderStatus(false, 'Not Synced', 'Not Synced - Invalid API key');
2079
2080 if (showAlerts) {
2081 showSyncError('❌ Invalid API Key', 'Please provide a valid ' + getPluginName() + ' API key.');
2082 }
2083
2084 } else if (response === null || !response.id) {
2085 // Update header status to "Not Synced" after failed sync
2086 if (!is_hb) {
2087 updateHeaderStatus(false, 'Not Synced', 'Not Synced - Data synchronization failed');
2088 }
2089
2090 if (showAlerts) {
2091 showSyncError('❌ Sync Failed', 'Something went wrong during synchronization. Please check your connection and try again.');
2092 }
2093 // Keep existing commented behavior for timestamp
2094
2095 } else {
2096 var dateString = dateFormat();
2097 $('#sendAuthTokenTimestamp').html(dateString);
2098 $('#sendAuthTokenTimestamp').css({ color: 'green' });
2099 // Server starts a 5-minute manual-sync cooldown after a successful sync.
2100 // Reflect that on the client so the button shows the cooldown across refreshes
2101 // instead of misleadingly saying "Sync Now" only to be rejected on next click.
2102 if (!is_hb) {
2103 var successCooldownMs = 5 * 60 * 1000;
2104 localStorage.setItem('metasync_manual_sync_throttle_expires', String(Date.now() + successCooldownMs));
2105 refreshManualSyncCooldownUI();
2106 }
2107
2108 // Update header status — check if UUID is present before declaring fully synced
2109 var hasOttoUuid = metaSync.otto_pixel_uuid && metaSync.otto_pixel_uuid.trim() !== '';
2110 if (hasOttoUuid) {
2111 updateHeaderStatus(true, 'Synced', 'Synced - Data synchronization completed successfully');
2112 } else {
2113 updateHeaderStatus(false, 'Warning', 'Connected but ' + getOttoName() + ' UUID is missing — deploys will not work. Please reconnect.');
2114 }
2115
2116 if (showAlerts) {
2117 showSyncSuccess('�
2118 Sync Complete', 'Your categories and user data have been successfully synchronized with ' + getPluginName() + '.');
2119 }
2120 }
2121 },
2122 error: function (xhr, status, error) {
2123 // Update header status to "Not Synced" for network errors
2124 if (!is_hb) {
2125 updateHeaderStatus(false, 'Not Synced', 'Not Synced - Network error during sync');
2126 }
2127
2128 // Reset button state
2129 if (showAlerts) {
2130 $('#sendAuthToken').prop('disabled', false).html('🔄 Sync Now');
2131 showSyncError('❌ Network Error', 'Failed to connect to ' + getPluginName() + '. Please check your internet connection and try again.');
2132 }
2133 }
2134 });
2135 }
2136
2137 /**
2138 * Show sync success notification (wrapper for consolidated function)
2139 */
2140 function showSyncSuccess(title, message) {
2141 showPluginNotice('success', title, message, 'metasync-sync-notice', 4000);
2142 }
2143
2144 /**
2145 * Show sync error notification (wrapper for consolidated function)
2146 */
2147 function showSyncError(title, message) {
2148 showPluginNotice('error', title, message, 'metasync-sync-error', 0);
2149 }
2150
2151 function clear_otto_caches() {
2152 jQuery.ajax({
2153 url: ajaxurl,
2154 type: 'GET',
2155 data: {
2156 action: 'metasync_clear_otto_cache',
2157 clear_otto_cache: 1
2158 },
2159 success: function (response) {
2160 const now = new Date();
2161 $('#clear_otto_caches').text('Cache Cleared ' + now.toLocaleTimeString());
2162 console.log('Cleared SSR Caches');
2163 }
2164 });
2165 }
2166
2167 // Holds the active manual-sync countdown interval so we can replace it
2168 // instead of stacking multiple timers if refreshManualSyncCooldownUI()
2169 // is called more than once (e.g. throttled response then refresh).
2170 var _manualSyncCountdownInterval = null;
2171
2172 /**
2173 * Read the manual-sync throttle expiry (client-time ms) from localStorage
2174 * and apply UI state: disable the button, show "Throttled (Nm)", and tick
2175 * every 15s. Idempotent — safe to call repeatedly.
2176 */
2177 function refreshManualSyncCooldownUI() {
2178 var $btn = $('#sendAuthToken');
2179 if ($btn.length === 0) {
2180 return;
2181 }
2182 if (_manualSyncCountdownInterval) {
2183 clearInterval(_manualSyncCountdownInterval);
2184 _manualSyncCountdownInterval = null;
2185 }
2186 var stored = localStorage.getItem('metasync_manual_sync_throttle_expires');
2187 if (!stored) {
2188 return;
2189 }
2190 var storedExpiry = parseInt(stored, 10);
2191 if (!storedExpiry || isNaN(storedExpiry)) {
2192 localStorage.removeItem('metasync_manual_sync_throttle_expires');
2193 return;
2194 }
2195 var nowMs = Date.now();
2196 if (storedExpiry <= nowMs) {
2197 localStorage.removeItem('metasync_manual_sync_throttle_expires');
2198 return;
2199 }
2200 var remainingMinutes = Math.max(1, Math.ceil((storedExpiry - nowMs) / 60000));
2201 // addClass('is-throttled') drives the visual state via CSS so it
2202 // doesn't depend on the disabled attribute being preserved by other
2203 // code paths in the AJAX callback chain (which strip it on success).
2204 $btn.prop('disabled', true).addClass('is-throttled').text('⏰ Throttled (' + remainingMinutes + 'm)');
2205 _manualSyncCountdownInterval = setInterval(function () {
2206 var currentNowMs = Date.now();
2207 if (storedExpiry <= currentNowMs) {
2208 clearInterval(_manualSyncCountdownInterval);
2209 _manualSyncCountdownInterval = null;
2210 localStorage.removeItem('metasync_manual_sync_throttle_expires');
2211 $btn.prop('disabled', false).removeClass('is-throttled').text('🔄 Sync Now');
2212 $('#sendAuthTokenTimestamp').text('Ready to sync').css({ color: 'green' });
2213 // Cooldown is over — clear the lingering "Request Throttled" admin
2214 // notice at the top of the page. Without this the button resets to
2215 // "Sync Now" but the red throttled bar stays visible (QA).
2216 $('.metasync-sync-notice, .metasync-sync-error').remove();
2217 return;
2218 }
2219 remainingMinutes = Math.max(1, Math.ceil((storedExpiry - currentNowMs) / 60000));
2220 $btn.text('⏰ Throttled (' + remainingMinutes + 'm)');
2221 }, 15000);
2222 }
2223
2224 jQuery(document).ready(function () {
2225
2226 // sendCustomerParams();
2227 $('#sendAuthToken').on('click', function (e) {
2228 e.preventDefault();
2229 sendCustomerParams();
2230
2231 });
2232
2233 // Restore manual-sync throttle countdown state across page refreshes.
2234 refreshManualSyncCooldownUI();
2235
2236 // handle otto clear cache button
2237 $('#clear_otto_caches').on('click', function (e){
2238 e.preventDefault();
2239 clear_otto_caches();
2240 });
2241
2242 // Handle General Setting Page form Submit
2243 $('#metaSyncGeneralSetting').on('submit', function (e) {
2244 // Check if this is a "Clear Error Logs" submission
2245 var formData = $(this).serialize();
2246 if (formData.indexOf('clear_log=yes') !== -1) {
2247 // This is a clear error logs submission - allow normal HTML form submission
2248 console.log('Clear Error Logs form detected - allowing HTML submission');
2249 return true; // Let the form submit normally
2250 }
2251
2252 // Check if this is a "Plugin Access Roles" submission - allow normal HTML form submission
2253 if (formData.indexOf('save_plugin_access_roles=yes') !== -1) {
2254 console.log('Plugin Access Roles form detected - allowing HTML submission');
2255 return true; // Let the form submit normally
2256 }
2257
2258 e.preventDefault(); // Prevent the default form submission for regular settings
2259 var actionField = $(this).find('input[name="action"]');
2260 var optionPage= $(this).find('input[name="option_page"]');
2261 var wpHttpReferer= $(this).find('input[name="_wp_http_referer"]');
2262 var wpnonce= $(this).find('input[name="_wpnonce"]');
2263 if(actionField.length > 0) {
2264 actionField.remove(); // Remove the action field if it exists
2265 optionPage.remove(); // Remove the action field if it exists
2266 wpHttpReferer.remove(); // Remove the action field if it exists
2267 wpnonce.remove(); // Remove the action field if it exists
2268 }
2269 // Re-serialize the form data after removing unwanted fields
2270 formData = $(this).serialize();
2271
2272 // Check if whitelabel data is in form data, if not add it manually
2273 if (formData.indexOf('whitelabel') === -1) {
2274 // Manually collect and add ALL whitelabel fields using helper function
2275 var whitelabelData = collectWhitelabelFields();
2276 if (whitelabelData) {
2277 formData += '&' + whitelabelData;
2278 }
2279 }
2280
2281 // Get current tab from URL
2282 var urlParams = new URLSearchParams(window.location.search);
2283 var currentTab = urlParams.get('tab') || 'general';
2284
2285 $.ajax({
2286 url: metaSync.ajax_url, // The AJAX URL provided by WordPress
2287 type: 'POST',
2288 data: formData + '&action=meta_sync_save_settings&active_tab=' + encodeURIComponent(currentTab), // Add the action and current tab
2289 success: function (response) {
2290 // Handle success response
2291 if(response.success){
2292 // Per-field whitelabel validation issues: the save succeeded for
2293 // other fields, so show a warning notice and stay on the page so the
2294 // user can see which values were rejected.
2295 const wlErrors = response.data && response.data.whitelabel_errors;
2296 if (wlErrors && Object.keys(wlErrors).length > 0) {
2297 var $warnNotice = $('<div>').addClass('notice notice-warning metasync-error-wrap');
2298 var $warnList = $('<ul>');
2299 Object.keys(wlErrors).forEach(function (fieldKey) {
2300 $warnList.append($('<li>').text(wlErrors[fieldKey]));
2301 });
2302 $warnNotice.append($('<p>').text('Settings saved, but some values were not applied:'));
2303 $warnNotice.append($warnList);
2304 $('.metasync-error-wrap').remove();
2305 $('#metaSyncGeneralSetting').before($warnNotice);
2306 $('html, body').animate({ scrollTop: 0 }, 'slow');
2307 return;
2308 }
2309
2310 // get value of input field white_label_plugin_menu_slug
2311 const whiteLableUrl = $('#metaSyncGeneralSetting input[name="metasync_options[general][white_label_plugin_menu_slug]"]').val();
2312 // check condition if it is empty or not and redirect it
2313
2314 // add the tag query to the window location
2315 let tabParam = new URLSearchParams(window.location.search).get('tab');
2316 let tabQuery = tabParam ? '&tab=' + encodeURIComponent(tabParam) : '';
2317
2318 // Prefer the server-sanitized slug so the redirect matches the page
2319 // WordPress actually registered; fall back to the raw input field value.
2320 const savedSlug = response.data && typeof response.data.saved_menu_slug === 'string' ? response.data.saved_menu_slug : null;
2321 const inputSlug = (whiteLableUrl && whiteLableUrl !== '') ? whiteLableUrl : 'searchatlas';
2322 const rawSlug = (savedSlug !== null) ? (savedSlug || 'searchatlas') : inputSlug;
2323 const pageSlug = encodeURIComponent(rawSlug.replace(/[^a-zA-Z0-9_\-]/g, '')) || 'searchatlas';
2324 var redirectUrl = metaSync.admin_url + '?page=' + pageSlug + tabQuery;
2325 try {
2326 var parsedRedirect = new URL(redirectUrl, window.location.origin);
2327 if (parsedRedirect.origin === window.location.origin) {
2328 var navLink = document.createElement('a');
2329 navLink.href = parsedRedirect.href;
2330 navLink.click();
2331 }
2332 } catch (e) { /* invalid URL */ }
2333 }else {
2334 // Handle error response
2335 const errors = response.data?.errors || [];
2336
2337 // Build error notice using DOM methods to avoid XSS
2338 var $errorNotice = $('<div>').addClass('notice notice-error metasync-error-wrap');
2339 if (Array.isArray(errors)) {
2340 var $ul = $('<ul>');
2341 errors.forEach(function (err) {
2342 $ul.append($('<li>').text(err));
2343 });
2344 $errorNotice.append($ul);
2345 }
2346
2347 // Remove previous error notices
2348 $('.metasync-error-wrap').remove();
2349
2350 // Insert the error message before the form
2351 $('#metaSyncGeneralSetting').before($errorNotice);
2352
2353 // Scroll to the top to ensure visibility
2354 $('html, body').animate({ scrollTop: 0 }, 'slow');
2355 }
2356
2357 },
2358 error: function (error) {
2359 // Handle error response
2360 alert('There was an error saving the settings.');
2361 console.log(error);
2362 }
2363 });
2364 });
2365
2366 //hook into heartbeat-send: client will send the message 'marco' in the 'client' var inside the data array
2367 jQuery(document).on('heartbeat-send', function (e, data) {
2368 e.preventDefault();
2369
2370 // adding heart beat label
2371 sendCustomerParams(true);
2372 });
2373
2374 //hook into heartbeat-tick: client looks for a 'server' var in the data array and logs it to console
2375 jQuery(document).on('heartbeat-tick', function (e, data) {
2376 // console.log('heartbeat-tick:', data);
2377 // if(data['server'])
2378 // console.log('Server: ' + data['server']);
2379 });
2380
2381 //hook into heartbeat-error: in case of error, let's log some stuff
2382 jQuery(document).on('heartbeat-error', function (e, jqXHR, textStatus, error) {
2383 console.log('BEGIN ERROR');
2384 console.log(textStatus);
2385 console.log(error);
2386 console.log('END ERROR');
2387 });
2388
2389 // Unsaved changes warning functionality
2390 var hasUnsavedChanges = false;
2391 var initialFormData = {};
2392
2393 // Check if we're on the Advanced tab (has its own save buttons per section)
2394 function isAdvancedTab() {
2395 return window.location.href.indexOf('tab=advanced') > -1;
2396 }
2397
2398 // Initialize form change detection
2399 function initializeUnsavedChangesDetection() {
2400 // Skip unsaved changes detection on Advanced tab - it has its own section-specific save buttons
2401 if (isAdvancedTab()) {
2402 return;
2403 }
2404
2405 var $forms = $('#metaSyncGeneralSetting, #metaSyncSeoControlsForm, form[method="post"][action*="options.php"]');
2406
2407 if ($forms.length === 0) {
2408 return; // No forms to track
2409 }
2410
2411 // Store initial form data
2412 $forms.each(function () {
2413 var formId = $(this).attr('id') || 'form_' + Math.random().toString(36).substr(2, 9);
2414 initialFormData[formId] = $(this).serialize();
2415 });
2416
2417 // Track changes on form inputs
2418 $forms.on('input change', 'input, select, textarea', function () {
2419 checkForChanges();
2420 });
2421
2422 // Special handling for media uploads and other dynamic changes
2423 $forms.on('DOMSubtreeModified', function () {
2424 setTimeout(checkForChanges, 100); // Small delay to allow DOM changes to complete
2425 });
2426 }
2427
2428 // Check if form data has changed
2429 function checkForChanges() {
2430 // Skip on Advanced tab
2431 if (isAdvancedTab()) {
2432 return;
2433 }
2434
2435 var $forms = $('#metaSyncGeneralSetting, form[method="post"][action*="options.php"]');
2436 var currentHasChanges = false;
2437
2438 $forms.each(function () {
2439 var formId = $(this).attr('id') || 'form_' + Math.random().toString(36).substr(2, 9);
2440 var currentData = $(this).serialize();
2441
2442 if (initialFormData[formId] && currentData !== initialFormData[formId]) {
2443 currentHasChanges = true;
2444 }
2445 });
2446
2447 hasUnsavedChanges = currentHasChanges;
2448 updateUnsavedChangesIndicator();
2449 }
2450
2451 // Update visual indicator for unsaved changes
2452 function updateUnsavedChangesIndicator() {
2453 var $saveButtons = $('input[type="submit"], button[type="submit"]').filter('[name="submit"], [value*="Save"]');
2454 var $forms = $('#metaSyncGeneralSetting, form[method="post"][action*="options.php"]');
2455
2456 if (hasUnsavedChanges) {
2457 // Add modern visual indicator to save buttons
2458 $saveButtons.each(function () {
2459 if (!$(this).find('.unsaved-indicator').length) {
2460 $(this).prepend('<span class="unsaved-indicator">●</span>');
2461 }
2462 });
2463
2464 // Add visual styling to forms
2465 $forms.addClass('has-unsaved-changes');
2466
2467 // Show sticky notification
2468 showUnsavedChangesNotification();
2469 } else {
2470 // Remove indicators
2471 $saveButtons.find('.unsaved-indicator').remove();
2472 $forms.removeClass('has-unsaved-changes');
2473
2474 // Hide sticky notification
2475 window.hideUnsavedChangesNotification();
2476 }
2477 }
2478
2479 // Show sticky notification for unsaved changes
2480 function showUnsavedChangesNotification() {
2481 var $notification = $('.metasync-unsaved-notification');
2482
2483 if ($notification.length === 0) {
2484 // Create notification if it doesn't exist
2485 var notificationHTML =
2486 '<div class="metasync-unsaved-notification">' +
2487 '<div class="notification-content">' +
2488 '<div class="notification-icon">●</div>' +
2489 '<div class="notification-message">You have unsaved changes</div>' +
2490 '</div>' +
2491 '<div class="notification-actions">' +
2492 '<button class="notification-button primary" onclick="saveChanges()">Save Now</button>' +
2493 '<button class="notification-button" onclick="discardChanges()">Discard</button>' +
2494 '<button class="close-notification" onclick="hideUnsavedChangesNotification()">×</button>' +
2495 '</div>' +
2496 '</div>';
2497
2498 $('body').append(notificationHTML);
2499 $notification = $('.metasync-unsaved-notification');
2500 }
2501
2502 // Show with animation
2503 setTimeout(function () {
2504 $notification.addClass('show');
2505 }, 100);
2506 }
2507
2508 // Hide sticky notification
2509 window.hideUnsavedChangesNotification = function () {
2510 var $notification = $('.metasync-unsaved-notification');
2511 $notification.removeClass('show');
2512 };
2513
2514 // Scroll to save button functionality
2515 window.scrollToSaveButton = function () {
2516 var $saveButton = $('input[type="submit"], button[type="submit"]').filter('[name="submit"], [value*="Save"]').first();
2517 if ($saveButton.length) {
2518 // Hide notification temporarily while scrolling
2519 window.hideUnsavedChangesNotification();
2520
2521 $('html, body').animate({
2522 scrollTop: $saveButton.offset().top - 100
2523 }, 500, function () {
2524 // Add highlight animation
2525 $saveButton.css('animation', 'save-button-highlight 1s ease-in-out');
2526
2527 // Remove animation after it completes
2528 setTimeout(function () {
2529 $saveButton.css('animation', '');
2530 }, 1000);
2531 });
2532 }
2533 };
2534
2535 // Discard changes functionality
2536 window.discardChanges = function () {
2537 if (confirm('Are you sure you want to discard all unsaved changes? This action cannot be undone.')) {
2538 // Reload the page to discard changes
2539 window.location.reload();
2540 }
2541 };
2542
2543
2544
2545 // Warning for in-page navigation (tab links)
2546 $('.metasync-nav-tab, .nav-tab').on('click', function (e) {
2547 if (hasUnsavedChanges) {
2548 var tabName = $(this).text().trim();
2549 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');
2550 if (!confirmed) {
2551 e.preventDefault();
2552 return false;
2553 } else {
2554 // If user confirms, clear the unsaved changes state
2555 hasUnsavedChanges = false;
2556 updateUnsavedChangesIndicator();
2557 }
2558 }
2559 });
2560
2561 // Clear unsaved changes flag when form is successfully submitted
2562 $('#metaSyncGeneralSetting').on('submit', function () {
2563 // Form submission handler already exists above, so we just need to listen for successful response
2564 var originalAjaxHandler = $(this).data('events') && $(this).data('events').submit;
2565 });
2566
2567 // Listen for successful form submission to clear the unsaved changes flag
2568 $(document).ajaxSuccess(function (event, xhr, settings) {
2569 if (settings.data && typeof settings.data === 'string' && settings.data.indexOf('action=meta_sync_save_settings') > -1) {
2570 try {
2571 var response = JSON.parse(xhr.responseText);
2572 if (response.success) {
2573 hasUnsavedChanges = false;
2574 updateUnsavedChangesIndicator();
2575 // Update initial form data after successful save
2576 var $forms = $('#metaSyncGeneralSetting, form[method="post"][action*="options.php"]');
2577 $forms.each(function () {
2578 var formId = $(this).attr('id') || 'form_' + Math.random().toString(36).substr(2, 9);
2579 initialFormData[formId] = $(this).serialize();
2580 });
2581 }
2582 } catch (e) {
2583 // Response is not JSON, ignore
2584 }
2585 }
2586 });
2587
2588 // Save Changes function - use AJAX instead of form submission
2589 window.saveChanges = function () {
2590 isSaving = true;
2591
2592 // Completely remove the floating notification immediately when clicked
2593 var $notification = $('.metasync-unsaved-notification');
2594 if ($notification.length > 0) {
2595 $notification.remove(); // Completely remove from DOM, no animations
2596 }
2597
2598 var $form = $('#metaSyncGeneralSetting');
2599 if ($form.length > 0) {
2600 // Get form data and submit via AJAX
2601 var formData = $form.serialize();
2602
2603 // Ensure ALL whitelabel data is included using helper function
2604 if (formData.indexOf('whitelabel') === -1) {
2605 var whitelabelData = collectWhitelabelFields();
2606 if (whitelabelData) {
2607 formData += '&' + whitelabelData;
2608 }
2609 }
2610
2611 // Get current tab from URL
2612 var urlParams = new URLSearchParams(window.location.search);
2613 var currentTab = urlParams.get('tab') || 'general';
2614
2615 formData += '&action=meta_sync_save_settings&active_tab=' + encodeURIComponent(currentTab);
2616
2617 $.ajax({
2618 url: metaSync.ajax_url,
2619 type: 'POST',
2620 data: formData,
2621 success: function (response) {
2622 if (response.success) {
2623 // Clear unsaved changes flag
2624 hasUnsavedChanges = false;
2625 updateUnsavedChangesIndicator();
2626
2627 // Clear any previous save notices (success, error, or warning)
2628 $('.metasync-save-notice').remove();
2629
2630 // Show temporary success indication in plugin area
2631 var noticeType, noticeMessage, noAutoDismiss = false;
2632 if (response.data && response.data.api_key_validated === true) {
2633 noticeType = 'notice-success';
2634 noticeMessage = $('<span/>').text('Settings saved successfully! API key verified & connected. Reloading\u2026').html();
2635 updateHeaderStatus(true, 'Synced', getPluginName() + ' connected');
2636 } else if (response.data && response.data.api_key_validated === false) {
2637 noticeType = 'notice-error';
2638 noticeMessage = $('<span/>').text(response.data.warning || 'The API key could not be verified. Your other settings were saved.').html();
2639 noAutoDismiss = true;
2640 // Revert the input field to the previously saved valid key from the backend
2641 var revertKey = (response.data.previous_api_key !== undefined) ? response.data.previous_api_key : '';
2642 $('#searchatlas-api-key').val(revertKey);
2643 } else if (response.data && response.data.api_key_removed === true) {
2644 // API key was cleared — show warning, update status, page will reload
2645 noticeType = 'notice-warning';
2646 noticeMessage = $('<span/>').text((response.data.warning || 'The API key has been removed.') + ' Reloading\u2026').html();
2647 updateHeaderStatus(false, 'Disconnected', getPluginName() + ' disconnected');
2648 } else if (response.data && response.data.warning) {
2649 // Network failure — key saved but unverified
2650 noticeType = 'notice-warning';
2651 noticeMessage = $('<span/>').text(response.data.warning).html();
2652 } else {
2653 noticeType = 'notice-success';
2654 noticeMessage = $('<span/>').text('Settings saved successfully!').html();
2655 }
2656 var successNotice = '<div class="notice ' + noticeType + ' is-dismissible metasync-save-notice" style="margin: 20px 0; padding: 12px;"><p><strong>' + noticeMessage + '</strong></p></div>';
2657
2658 // Insert between navigation menu and page content
2659 var $navWrapper = $('.metasync-nav-wrapper');
2660 if ($navWrapper.length > 0) {
2661 // Position after navigation menu but before first dashboard card or form
2662 $navWrapper.after(successNotice);
2663 } else {
2664 // Fallback: insert at top of settings page
2665 $('.metasync-dashboard-wrap').prepend(successNotice);
2666 }
2667
2668 // Scroll to the notice for better visibility
2669 $('html, body').animate({ scrollTop: 0 }, 'slow');
2670
2671 // Reload page when API key status changed so server-rendered
2672 // sections (One-Click Authentication, promo sidebar) update
2673 if (response.data && (response.data.api_key_validated === true || response.data.api_key_removed === true)) {
2674 setTimeout(function () {
2675 location.reload();
2676 }, 1500);
2677 return;
2678 }
2679
2680 // Auto-dismiss success/warning notices; keep errors visible
2681 if (!noAutoDismiss) {
2682 setTimeout(function () {
2683 $('.metasync-save-notice').fadeOut(300, function () {
2684 $(this).remove();
2685 });
2686 }, 3000);
2687 }
2688 } else {
2689 // Handle validation errors
2690 var errors = response.data && response.data.errors ? response.data.errors : [];
2691
2692 // Build error notice using DOM methods to avoid XSS
2693 var $errorNotice = $('<div>').addClass('notice notice-error metasync-error-wrap').css({ margin: '20px', padding: '12px' });
2694 if (Array.isArray(errors)) {
2695 var $ul = $('<ul>');
2696 for (var i = 0; i < errors.length; i++) {
2697 $ul.append($('<li>').text(errors[i]));
2698 }
2699 $errorNotice.append($ul);
2700 } else {
2701 var message = 'An error occurred while saving settings.';
2702 if (response.data && response.data.message) {
2703 message = response.data.message;
2704 }
2705 $errorNotice.append($('<p>').text(message));
2706 }
2707
2708 // Insert error notice in plugin area
2709 var $navWrapper = $('.metasync-nav-wrapper');
2710 if ($navWrapper.length > 0) {
2711 $navWrapper.after($errorNotice);
2712 } else {
2713 // Fallback: insert at top of plugin content area
2714 $('.metasync-dashboard-wrap').prepend($errorNotice);
2715 }
2716
2717 // Scroll to the error message for better visibility
2718 $('html, body').animate({ scrollTop: 0 }, 'slow');
2719
2720 setTimeout(function () {
2721 $('.metasync-error-wrap').fadeOut(300, function () {
2722 $(this).remove();
2723 });
2724 }, 5000);
2725 }
2726 isSaving = false;
2727 },
2728 error: function (xhr, status, error) {
2729 // Handle AJAX error
2730 var errorMessage = 'There was an error saving the settings. Please try again.';
2731 if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) {
2732 errorMessage = xhr.responseJSON.data.message;
2733 }
2734
2735 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>';
2736
2737 // Insert between navigation menu and page content
2738 var $navWrapper = $('.metasync-nav-wrapper');
2739 if ($navWrapper.length > 0) {
2740 // Position after navigation menu but before first dashboard card or form
2741 $navWrapper.after(ajaxErrorNotice);
2742 } else {
2743 // Fallback: insert at top of settings page
2744 $('.metasync-dashboard-wrap').prepend(ajaxErrorNotice);
2745 }
2746
2747 // Scroll to the error message for better visibility
2748 $('html, body').animate({ scrollTop: 0 }, 'slow');
2749
2750 setTimeout(function () {
2751 $('.metasync-ajax-error').fadeOut(300, function () {
2752 $(this).remove();
2753 });
2754 }, 5000);
2755
2756 isSaving = false;
2757 }
2758 });
2759 }
2760 };
2761
2762 // Enhanced beforeunload message (disabled when saving)
2763 var isSaving = false;
2764 $(window).on('beforeunload', function (e) {
2765 if (hasUnsavedChanges && !isSaving) {
2766 var message = '🔄 You have unsaved changes in MetaSync settings that will be lost if you leave this page.';
2767 e.returnValue = message; // For older browsers
2768 return message;
2769 }
2770 });
2771
2772 // Initialize the detection when page loads
2773 setTimeout(initializeUnsavedChangesDetection, 1000); // Small delay to ensure all elements are loaded
2774
2775 // Indexation Control form AJAX save functionality
2776 function initializeSeoControlsSaveHandler() {
2777 // Only initialize if we're on the Indexation Control page
2778 if ($('#metaSyncSeoControlsForm').length > 0) {
2779 // Override the saveChanges function for Indexation Control form
2780 window.saveChanges = function () {
2781 var $form = $('#metaSyncSeoControlsForm');
2782 if ($form.length > 0) {
2783 // Get form data and submit via AJAX
2784 var formData = $form.serialize();
2785 formData += '&action=meta_sync_save_seo_controls';
2786
2787 // Remove the notification immediately
2788 var $notification = $('.metasync-unsaved-notification');
2789 if ($notification.length > 0) {
2790 $notification.remove();
2791 }
2792
2793 // Clear previous messages
2794 $('#seo-controls-messages').empty();
2795
2796 $.ajax({
2797 url: ajaxurl || metaSync.ajax_url,
2798 type: 'POST',
2799 data: formData,
2800 dataType: 'json',
2801 success: function (response) {
2802 if (response.success) {
2803 // Show success message above Indexation Control section
2804 var $successNotice = $('<div>').addClass('notice notice-success is-dismissible').css('margin-bottom', '20px');
2805 var $successP = $('<p>');
2806 $successP.append($('<strong>').text('�
2807 Success! '));
2808 $successP[0].appendChild(document.createTextNode(response.data.message));
2809 $successNotice.append($successP);
2810 $('#seo-controls-messages').empty().append($successNotice);
2811
2812 // Clear unsaved changes state
2813 hasUnsavedChanges = false;
2814 if (typeof updateUnsavedChangesIndicator === 'function') {
2815 updateUnsavedChangesIndicator();
2816 }
2817
2818 // Update initial form data to current state after successful save
2819 var formId = $form.attr('id') || 'metaSyncSeoControlsForm';
2820 initialFormData[formId] = $form.serialize();
2821
2822 // Auto-hide success notice after 5 seconds
2823 setTimeout(function () {
2824 $('#seo-controls-messages .notice-success').fadeOut();
2825 }, 5000);
2826 } else {
2827 // Show error message above Indexation Control section
2828 var $errorNoticeCtrl = $('<div>').addClass('notice notice-error is-dismissible').css('margin-bottom', '20px');
2829 var $errorPCtrl = $('<p>');
2830 $errorPCtrl.append($('<strong>').text('❌ Error! '));
2831 $errorPCtrl[0].appendChild(document.createTextNode(response.data.message || 'Failed to save settings'));
2832 $errorNoticeCtrl.append($errorPCtrl);
2833 $('#seo-controls-messages').empty().append($errorNoticeCtrl);
2834 }
2835
2836 // Make dismiss buttons work
2837 $('#seo-controls-messages .notice-dismissible').each(function () {
2838 var $notice = $(this);
2839 if (!$notice.find('.notice-dismiss').length) {
2840 $notice.append('<button type="button" class="notice-dismiss"><span class="screen-reader-text">Dismiss this notice.</span></button>');
2841 }
2842 $notice.find('.notice-dismiss').on('click', function () {
2843 $notice.fadeOut();
2844 });
2845 });
2846 },
2847 error: function (xhr, status, error) {
2848 // Show error message above Indexation Control section
2849 $('#seo-controls-messages').html(
2850 '<div class="notice notice-error is-dismissible" style="margin-bottom: 20px;">' +
2851 '<p><strong>❌ Error!</strong> Network error occurred while saving. Please check your connection and try again.</p>' +
2852 '</div>'
2853 );
2854 console.error('AJAX Error:', error);
2855 console.error('XHR Status:', xhr.status);
2856 console.error('XHR Response:', xhr.responseText);
2857
2858 // Don't clear unsaved changes on network error
2859 // User may want to retry or fix the issue
2860 },
2861 complete: function () {
2862 // Re-enable save button if it was disabled
2863 $('.metasync-save-button').prop('disabled', false);
2864 }
2865 });
2866 } else {
2867 // Fallback to regular form submission
2868 $('form[method="post"]').first().submit();
2869 }
2870 };
2871 }
2872 }
2873
2874 // Initialize Indexation Control functionality
2875 initializeSeoControlsSaveHandler();
2876
2877 /**
2878 * Handle "All Roles" checkbox behavior for Content Genius sync
2879 * When "All Roles" is checked, uncheck all other role checkboxes
2880 * When any specific role is checked, uncheck "All Roles"
2881 *
2882 * @since 1.0.0
2883 * @returns {void}
2884 */
2885 function initializeContentGeniusRoleCheckboxes() {
2886 try {
2887 // Cache selectors for better performance
2888 var allRolesCheckbox = $('input[name="metasync_options[general][content_genius_sync_roles][]"][value="all"]');
2889 var roleCheckboxes = $('input[name="metasync_options[general][content_genius_sync_roles][]"]').not('[value="all"]');
2890
2891 // Exit early if elements don't exist
2892 if (allRolesCheckbox.length === 0 && roleCheckboxes.length === 0) {
2893 return;
2894 }
2895
2896 // When "All Roles" is checked, uncheck all other checkboxes
2897 if (allRolesCheckbox.length > 0) {
2898 allRolesCheckbox.off('change.metasyncRoles').on('change.metasyncRoles', function () {
2899 try {
2900 if ($(this).is(':checked')) {
2901 roleCheckboxes.prop('checked', false);
2902 // Visual feedback
2903 roleCheckboxes.closest('.metasync-role-option').removeClass('active');
2904 $(this).closest('.metasync-role-option-all').addClass('active');
2905 } else {
2906 $(this).closest('.metasync-role-option-all').removeClass('active');
2907 }
2908 } catch (e) {
2909 console.warn('MetaSync: Error handling "All Roles" checkbox change:', e);
2910 }
2911 });
2912 }
2913
2914 // When any specific role is checked, uncheck "All Roles"
2915 if (roleCheckboxes.length > 0) {
2916 roleCheckboxes.off('change.metasyncRoles').on('change.metasyncRoles', function () {
2917 try {
2918 if ($(this).is(':checked')) {
2919 allRolesCheckbox.prop('checked', false);
2920 allRolesCheckbox.closest('.metasync-role-option-all').removeClass('active');
2921 // Visual feedback
2922 $(this).closest('.metasync-role-option').addClass('active');
2923 } else {
2924 $(this).closest('.metasync-role-option').removeClass('active');
2925 }
2926 } catch (e) {
2927 console.warn('MetaSync: Error handling role checkbox change:', e);
2928 }
2929 });
2930 }
2931
2932 // Initialize active states on page load
2933 initializeRoleCheckboxStates();
2934
2935 } catch (error) {
2936 console.error('MetaSync: Failed to initialize Content Genius role checkboxes:', error);
2937 }
2938 }
2939
2940 /**
2941 * Initialize active states for role checkboxes on page load
2942 *
2943 * @since 1.0.0
2944 * @returns {void}
2945 */
2946 function initializeRoleCheckboxStates() {
2947 try {
2948 var allRolesCheckbox = $('input[name="metasync_options[general][content_genius_sync_roles][]"][value="all"]');
2949 var roleCheckboxes = $('input[name="metasync_options[general][content_genius_sync_roles][]"]').not('[value="all"]');
2950
2951 // Set active state for "All Roles" if checked
2952 if (allRolesCheckbox.is(':checked')) {
2953 allRolesCheckbox.closest('.metasync-role-option-all').addClass('active');
2954 }
2955
2956 // Set active state for checked roles
2957 roleCheckboxes.each(function () {
2958 if ($(this).is(':checked')) {
2959 $(this).closest('.metasync-role-option').addClass('active');
2960 }
2961 });
2962 } catch (e) {
2963 console.warn('MetaSync: Error initializing role checkbox states:', e);
2964 }
2965 }
2966
2967 // Initialize Content Genius role checkboxes with safety wrapper
2968 if (typeof $ !== 'undefined' && $.fn) {
2969 initializeContentGeniusRoleCheckboxes();
2970 } else {
2971 console.warn('MetaSync: jQuery not available for role checkbox initialization');
2972 }
2973
2974 // ========================================
2975 // SETTINGS ACCORDION FUNCTIONALITY
2976 // ========================================
2977
2978 /**
2979 * Initialize accordion sections with localStorage state persistence
2980 */
2981 function initSettingsAccordion() {
2982 var $accordionSections = $('.metasync-accordion-section');
2983
2984 if ($accordionSections.length === 0) {
2985 return; // No accordion on this page
2986 }
2987
2988 console.log('🎨 Initializing settings accordion with ' + $accordionSections.length + ' sections');
2989
2990 // Restore saved state from localStorage
2991 restoreAccordionState();
2992
2993 // Click handler for accordion headers
2994 $('.metasync-accordion-header').on('click', function (e) {
2995 toggleAccordionSection($(this));
2996 });
2997
2998 // Keyboard navigation (Enter/Space)
2999 $('.metasync-accordion-header').on('keydown', function (e) {
3000 if (e.key === 'Enter' || e.key === ' ') {
3001 e.preventDefault();
3002 toggleAccordionSection($(this));
3003 }
3004 });
3005 }
3006
3007 /**
3008 * Toggle accordion section open/closed
3009 * @param {jQuery} $header - The clicked header element
3010 */
3011 function toggleAccordionSection($header) {
3012 var $section = $header.closest('.metasync-accordion-section');
3013 var $content = $section.find('.metasync-accordion-content');
3014 var isOpen = $header.attr('aria-expanded') === 'true';
3015 var sectionKey = $section.data('section');
3016
3017 if (isOpen) {
3018 // Close section
3019 $header.attr('aria-expanded', 'false');
3020 $content.attr('data-state', 'closed');
3021 console.log('📁 Closed accordion section: ' + sectionKey);
3022 } else {
3023 // Open section
3024 $header.attr('aria-expanded', 'true');
3025 $content.attr('data-state', 'open');
3026 console.log('📂 Opened accordion section: ' + sectionKey);
3027 }
3028
3029 // Save state to localStorage
3030 saveAccordionState();
3031 }
3032
3033 /**
3034 * Save accordion state to localStorage
3035 */
3036 function saveAccordionState() {
3037 var state = {};
3038
3039 $('.metasync-accordion-section').each(function () {
3040 var sectionKey = $(this).data('section');
3041 var isOpen = $(this).find('.metasync-accordion-header').attr('aria-expanded') === 'true';
3042 state[sectionKey] = isOpen;
3043 });
3044
3045 try {
3046 localStorage.setItem('metasync_accordion_state', JSON.stringify(state));
3047 } catch (e) {
3048 console.warn('⚠️ Could not save accordion state to localStorage:', e);
3049 }
3050 }
3051
3052 /**
3053 * Restore accordion state from localStorage
3054 */
3055 function restoreAccordionState() {
3056 try {
3057 var savedState = localStorage.getItem('metasync_accordion_state');
3058 if (!savedState) {
3059 return; // No saved state, use defaults
3060 }
3061
3062 var state = JSON.parse(savedState);
3063
3064 $('.metasync-accordion-section').each(function () {
3065 var $section = $(this);
3066 var sectionKey = $section.data('section');
3067 var $header = $section.find('.metasync-accordion-header');
3068 var $content = $section.find('.metasync-accordion-content');
3069
3070 if (Object.prototype.hasOwnProperty.call(state, sectionKey)) {
3071 var shouldBeOpen = state[sectionKey];
3072 $header.attr('aria-expanded', shouldBeOpen ? 'true' : 'false');
3073 $content.attr('data-state', shouldBeOpen ? 'open' : 'closed');
3074 }
3075 });
3076
3077 console.log('💾 Restored accordion state from localStorage');
3078 } catch (e) {
3079 console.warn('⚠️ Could not restore accordion state:', e);
3080 }
3081 }
3082
3083 // Initialize accordion when DOM is ready
3084 initSettingsAccordion();
3085
3086 // ========================================
3087 // TOOLTIP SYSTEM
3088 // ========================================
3089
3090 /**
3091 * Initialize tooltip functionality
3092 */
3093 function initTooltipSystem() {
3094 console.log('🔍 Tooltip system initialization started');
3095
3096 var $tooltipTriggers = $('.metasync-tooltip-trigger');
3097 var currentTooltip = null;
3098
3099 console.log('🔍 Found ' + $tooltipTriggers.length + ' tooltip triggers');
3100
3101 if ($tooltipTriggers.length === 0) {
3102 console.warn('⚠️ No tooltip triggers found on this page');
3103 return; // No tooltips on this page
3104 }
3105
3106 console.log('💡 Initializing tooltip system with ' + $tooltipTriggers.length + ' tooltips');
3107
3108 // Click handler for tooltip triggers
3109 $tooltipTriggers.on('click', function (e) {
3110 e.preventDefault();
3111 e.stopPropagation();
3112
3113 var $trigger = $(this);
3114 var tooltipId = $trigger.data('tooltip-id');
3115 var $tooltip = $('#tooltip-' + tooltipId);
3116
3117 console.log('🖱️ Tooltip trigger clicked:', tooltipId);
3118 console.log('🎯 Tooltip element found:', $tooltip.length);
3119
3120 // Close other tooltips
3121 if (currentTooltip && currentTooltip[0] !== $tooltip[0]) {
3122 currentTooltip.removeClass('show');
3123 }
3124
3125 // Toggle current tooltip
3126 if ($tooltip.hasClass('show')) {
3127 $tooltip.removeClass('show');
3128 currentTooltip = null;
3129 } else {
3130 $tooltip.addClass('show');
3131 currentTooltip = $tooltip;
3132 positionTooltip($trigger, $tooltip);
3133 }
3134 });
3135
3136 // Hover handler (desktop only)
3137 var hideTimeout;
3138
3139 if (window.innerWidth > 768) {
3140 // Show tooltip on trigger hover
3141 $tooltipTriggers.on('mouseenter', function () {
3142 var $trigger = $(this);
3143 var tooltipId = $trigger.data('tooltip-id');
3144 var $tooltip = $('#tooltip-' + tooltipId);
3145
3146 // Clear any pending hide timeout
3147 clearTimeout(hideTimeout);
3148
3149 console.log('🖱️ Hover on trigger:', tooltipId);
3150
3151 // Close other tooltips
3152 if (currentTooltip && currentTooltip[0] !== $tooltip[0]) {
3153 currentTooltip.removeClass('show');
3154 }
3155
3156 $tooltip.addClass('show');
3157 currentTooltip = $tooltip;
3158 positionTooltip($trigger, $tooltip);
3159 });
3160
3161 // Start hide timer when leaving trigger
3162 $tooltipTriggers.on('mouseleave', function () {
3163 var tooltipId = $(this).data('tooltip-id');
3164 var $tooltip = $('#tooltip-' + tooltipId);
3165
3166 console.log('🖱️ Mouse left trigger:', tooltipId);
3167
3168 // Delay hiding to allow moving to tooltip
3169 hideTimeout = setTimeout(function () {
3170 // Only hide if not hovering tooltip
3171 if (!$tooltip.is(':hover')) {
3172 console.log('⏱️ Hiding tooltip:', tooltipId);
3173 $tooltip.removeClass('show');
3174 if (currentTooltip && currentTooltip[0] === $tooltip[0]) {
3175 currentTooltip = null;
3176 }
3177 } else {
3178 console.log('✋ Mouse is over tooltip, keeping visible');
3179 }
3180 }, 200);
3181 });
3182
3183 // Cancel hide when entering tooltip
3184 $('.metasync-tooltip').on('mouseenter', function () {
3185 console.log('🎯 Mouse entered tooltip');
3186 clearTimeout(hideTimeout);
3187 $(this).addClass('show');
3188 });
3189
3190 // Hide when leaving tooltip
3191 $('.metasync-tooltip').on('mouseleave', function () {
3192 console.log('🎯 Mouse left tooltip');
3193 var $tooltip = $(this);
3194
3195 hideTimeout = setTimeout(function () {
3196 var tooltipId = $tooltip.attr('id').replace('tooltip-', '');
3197 var $trigger = $('[data-tooltip-id="' + tooltipId + '"]');
3198
3199 // Only hide if not hovering trigger
3200 if (!$trigger.is(':hover')) {
3201 console.log('⏱️ Hiding tooltip from tooltip leave');
3202 $tooltip.removeClass('show');
3203 if (currentTooltip && currentTooltip[0] === $tooltip[0]) {
3204 currentTooltip = null;
3205 }
3206 } else {
3207 console.log('✋ Mouse is back on trigger, keeping visible');
3208 }
3209 }, 200);
3210 });
3211 }
3212
3213 // Close tooltip when clicking outside
3214 $(document).on('click', function (e) {
3215 if (!$(e.target).closest('.metasync-tooltip-trigger, .metasync-tooltip').length) {
3216 if (currentTooltip) {
3217 currentTooltip.removeClass('show');
3218 currentTooltip = null;
3219 }
3220 }
3221 });
3222
3223 // Keyboard accessibility - ESC to close
3224 $(document).on('keydown', function (e) {
3225 if (e.key === 'Escape' && currentTooltip) {
3226 currentTooltip.removeClass('show');
3227 currentTooltip = null;
3228 }
3229 });
3230
3231 // Reposition tooltips on window resize
3232 $(window).on('resize', function () {
3233 if (currentTooltip) {
3234 var tooltipId = currentTooltip.attr('id').replace('tooltip-', '');
3235 var $trigger = $('[data-tooltip-id="' + tooltipId + '"]');
3236 positionTooltip($trigger, currentTooltip);
3237 }
3238 });
3239 }
3240
3241 /**
3242 * Position tooltip relative to trigger
3243 * @param {jQuery} $trigger - The trigger button
3244 * @param {jQuery} $tooltip - The tooltip element
3245 */
3246 function positionTooltip($trigger, $tooltip) {
3247 // Skip positioning on mobile (uses fixed positioning)
3248 if (window.innerWidth <= 768) {
3249 return;
3250 }
3251
3252 var triggerRect = $trigger[0].getBoundingClientRect();
3253 var tooltipWidth = $tooltip.outerWidth();
3254 var viewportWidth = $(window).width();
3255 var spaceRight = viewportWidth - triggerRect.right;
3256
3257 // Check if tooltip would overflow on the right
3258 if (spaceRight < tooltipWidth + 20) {
3259 // Position on the left side
3260 $tooltip.attr('data-position', 'left');
3261 } else {
3262 // Position on the right side (default)
3263 $tooltip.attr('data-position', 'right');
3264 }
3265 }
3266
3267 // Initialize tooltip system
3268 initTooltipSystem();
3269
3270 // PR3: Burst ping — 10 min polling when UNREGISTERED or KEY_PENDING. Stop after 5 failed attempts.
3271 (function () {
3272 if (typeof metaSync === 'undefined' || !metaSync.heartbeat_state) {
3273 return;
3274 }
3275 var state = metaSync.heartbeat_state;
3276 if (state !== 'UNREGISTERED' && state !== 'KEY_PENDING') {
3277 return;
3278 }
3279
3280 var INTERVAL_MS = 10 * 60 * 1000;
3281 var MAX_ATTEMPTS = 5;
3282 var attempts = 0;
3283 var lastState = state;
3284 var intervalId = null;
3285
3286 function stop() {
3287 if (intervalId) {
3288 clearInterval(intervalId);
3289 intervalId = null;
3290 }
3291 }
3292
3293 intervalId = setInterval(function () {
3294 $.post(metaSync.ajax_url, {
3295 action: 'metasync_burst_ping',
3296 nonce: metaSync.burst_ping_nonce
3297 })
3298 .done(function (res) {
3299 if (!res || !res.data) {
3300 attempts++;
3301 if (attempts >= MAX_ATTEMPTS) {
3302 stop();
3303 }
3304 return;
3305 }
3306 var data = res.data;
3307 var newState = data.state || lastState;
3308 if (newState !== lastState) {
3309 attempts = 0;
3310 lastState = newState;
3311 }
3312 if (data.heartbeat_confirmed || newState === 'CONNECTED') {
3313 stop();
3314 if (newState === 'CONNECTED' && typeof updateHeaderStatus === 'function') {
3315 var hasOttoUuid = typeof metaSync !== 'undefined' && metaSync.otto_pixel_uuid && metaSync.otto_pixel_uuid.trim() !== '';
3316 if (hasOttoUuid) {
3317 updateHeaderStatus(true, 'Synced', 'Heartbeat confirmed');
3318 } else {
3319 updateHeaderStatus(false, 'Warning', 'Connected but OTTO UUID is missing — deploys will not work. Please reconnect.');
3320 }
3321 }
3322 return;
3323 }
3324 attempts++;
3325 if (attempts >= MAX_ATTEMPTS) {
3326 stop();
3327 }
3328 })
3329 .fail(function () {
3330 attempts++;
3331 if (attempts >= MAX_ATTEMPTS) {
3332 stop();
3333 }
3334 });
3335 }, INTERVAL_MS);
3336 })();
3337
3338 });
3339
3340 })(jQuery);
3341