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

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

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