Plugin Directory

Changeset 3401441


Ignore:
Timestamp:
11/23/2025 09:37:37 PM (7 weeks ago)
Author:
optiuser
Message:
  • Fix: Removed all debug error_log() calls from production code
  • Fix: Replaced date() with gmdate() for timezone-safe date handling
  • Fix: Added translator comments for i18n compliance
  • Fix: Updated API URL from localhost to production endpoint
  • Fix: Corrected stable tag version mismatch
  • Enhancement: Improved readme with better descriptions and FAQ
  • Enhancement: Added Plugin URI and updated Author URI
  • Compatibility: Full WordPress 6.8 compatibility verified
  • Enhancement: Added COALESCE for better handling of NULL titles in Top Pages
  • Enhancement: Improved country detection with browser language fallback when IP geolocation fails
  • Enhancement: Top Pages widget now displays page views instead of clicks for better accuracy
Location:
opti-behavior
Files:
96 added
25 edited

Legend:

Unmodified
Added
Removed
  • opti-behavior/trunk/Opti-Behavior.php

    r3399182 r3401441  
    22/**
    33 * Plugin Name: Opti-Behavior
    4  * Description: Transform your WordPress site with powerful analytics! Track user behavior with beautiful heatmaps and real-time insights. Boost conversions and optimize user experience with data-driven decisions. Upgrade to PRO for session recordings and advanced features.
    5  * Version:     1.0.4
     4 * Plugin URI:  https://optiuser.com/
     5 * Description: Transform your WordPress site with powerful analytics! Track user behavior with beautiful heatmaps and real-time insights. Boost conversions and optimize user experience with data-driven decisions.
     6 * Version:     1.0.5
    67 * Author:      OptiUser
    7  * Author URI:  https://profiles.wordpress.org/optiuser/
     8 * Author URI:  https://optiuser.com/
    89 * License:     GPLv2 or later
    910 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    1213 *
    1314 * @package opti-behavior
    14  * @copyright 2025 Opti-User
    15  * @version 1.0.4
     15 * @copyright 2025 OptiUser
     16 * @version 1.0.5
    1617 */
    1718
     
    7980
    8081// Define plugin constants.
    81 define( 'OPTI_BEHAVIOR_HEATMAP_VERSION', '1.0.4' );
     82define( 'OPTI_BEHAVIOR_HEATMAP_VERSION', '1.0.5' );
    8283define( 'OPTI_BEHAVIOR_HEATMAP_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
    8384define( 'OPTI_BEHAVIOR_HEATMAP_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
  • opti-behavior/trunk/admin/class-opti-behavior-heatmap-ajax-handler.php

    r3399182 r3401441  
    8181        add_action( 'wp_ajax_optibehavior_top_users', array( $this, 'ajax_top_users' ) );
    8282        add_action( 'wp_ajax_optibehavior_backfill_pageview_times', array( $this, 'ajax_backfill_pageview_times' ) );
     83
     84        // Support form email handler
     85        add_action( 'wp_ajax_opti_behavior_send_support_email', array( $this, 'ajax_send_support_email' ) );
    8386    }
    8487
     
    11421145
    11431146        if ( is_wp_error( $response ) ) {
    1144             // API request failed, return unknown
     1147            // API request failed, try browser language fallback
     1148            $browser_geo = $this->get_country_from_browser_language();
     1149            if ( ! empty( $browser_geo['country'] ) && $browser_geo['country'] !== 'UN' ) {
     1150                $result = array(
     1151                    'country'      => $browser_geo['country'],
     1152                    'country_name' => $browser_geo['country_name'],
     1153                    'region'       => 'Unknown',
     1154                    'city'         => 'Unknown',
     1155                    'timezone'     => '',
     1156                    'source'       => 'browser_language_fallback',
     1157                );
     1158                // Cache for shorter time on fallback (15 minutes)
     1159                set_transient( $cache_key, $result, 15 * MINUTE_IN_SECONDS );
     1160                return $result;
     1161            }
     1162
     1163            // Last resort: return unknown
    11451164            $result = array(
    11461165                'country'      => 'UN',
     
    11741193        }
    11751194
    1176         // API returned error or invalid data
     1195        // API returned error or invalid data, try browser language fallback
     1196        $browser_geo = $this->get_country_from_browser_language();
     1197        if ( ! empty( $browser_geo['country'] ) && $browser_geo['country'] !== 'UN' ) {
     1198            $result = array(
     1199                'country'      => $browser_geo['country'],
     1200                'country_name' => $browser_geo['country_name'],
     1201                'region'       => 'Unknown',
     1202                'city'         => 'Unknown',
     1203                'timezone'     => '',
     1204                'source'       => 'browser_language_fallback',
     1205            );
     1206            // Cache for shorter time on fallback (15 minutes)
     1207            set_transient( $cache_key, $result, 15 * MINUTE_IN_SECONDS );
     1208            return $result;
     1209        }
     1210
     1211        // Last resort: return unknown
    11771212        $result = array(
    11781213            'country'      => 'UN',
     
    21612196    }
    21622197
     2198    /**
     2199     * AJAX handler for sending support emails.
     2200     *
     2201     * @since 1.0.4
     2202     */
     2203    public function ajax_send_support_email() {
     2204        // Verify nonce
     2205        if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'opti_behavior_support_email' ) ) {
     2206            wp_send_json_error( array( 'message' => __( 'Security check failed', 'opti-behavior' ) ) );
     2207        }
     2208
     2209        // Verify user is logged in
     2210        if ( ! is_user_logged_in() ) {
     2211            wp_send_json_error( array( 'message' => __( 'You must be logged in to send a message', 'opti-behavior' ) ) );
     2212        }
     2213
     2214        // Sanitize and validate inputs
     2215        $name    = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '';
     2216        $email   = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
     2217        $subject = isset( $_POST['subject'] ) ? sanitize_text_field( wp_unslash( $_POST['subject'] ) ) : '';
     2218        $message = isset( $_POST['message'] ) ? sanitize_textarea_field( wp_unslash( $_POST['message'] ) ) : '';
     2219
     2220        // Validate required fields
     2221        if ( empty( $name ) || empty( $email ) || empty( $subject ) || empty( $message ) ) {
     2222            wp_send_json_error( array( 'message' => __( 'Please fill in all fields', 'opti-behavior' ) ) );
     2223        }
     2224
     2225        // Validate email
     2226        if ( ! is_email( $email ) ) {
     2227            wp_send_json_error( array( 'message' => __( 'Please enter a valid email address', 'opti-behavior' ) ) );
     2228        }
     2229
     2230        // Prepare email
     2231        $to      = '[email protected]';
     2232        $headers = array(
     2233            'Content-Type: text/html; charset=UTF-8',
     2234            'From: ' . $name . ' <' . $email . '>',
     2235            'Reply-To: ' . $email,
     2236        );
     2237
     2238        // Build email body
     2239        $email_body = '<html><body>';
     2240        $email_body .= '<h2>' . esc_html__( 'New Support Message from Opti-Behavior Plugin', 'opti-behavior' ) . '</h2>';
     2241        $email_body .= '<p><strong>' . esc_html__( 'Name:', 'opti-behavior' ) . '</strong> ' . esc_html( $name ) . '</p>';
     2242        $email_body .= '<p><strong>' . esc_html__( 'Email:', 'opti-behavior' ) . '</strong> ' . esc_html( $email ) . '</p>';
     2243        $email_body .= '<p><strong>' . esc_html__( 'Subject:', 'opti-behavior' ) . '</strong> ' . esc_html( $subject ) . '</p>';
     2244        $email_body .= '<p><strong>' . esc_html__( 'Message:', 'opti-behavior' ) . '</strong></p>';
     2245        $email_body .= '<p>' . nl2br( esc_html( $message ) ) . '</p>';
     2246        $email_body .= '<hr>';
     2247        $email_body .= '<p><small>' . esc_html__( 'Sent from:', 'opti-behavior' ) . ' ' . esc_url( get_site_url() ) . '</small></p>';
     2248        $email_body .= '</body></html>';
     2249
     2250        // Send email
     2251        $sent = wp_mail( $to, '[Opti-Behavior Support] ' . $subject, $email_body, $headers );
     2252
     2253        if ( $sent ) {
     2254            wp_send_json_success( array(
     2255                'message' => __( 'Your message has been sent successfully! We\'ll get back to you soon.', 'opti-behavior' ),
     2256            ) );
     2257        } else {
     2258            // If wp_mail fails, provide fallback message
     2259            wp_send_json_error( array(
     2260                'message' => sprintf(
     2261                    /* translators: %s: support email address */
     2262                    __( 'We couldn\'t send your message automatically. Please email us directly at %s', 'opti-behavior' ),
     2263                    '<a href="mailto:[email protected]">[email protected]</a>'
     2264                ),
     2265            ) );
     2266        }
     2267    }
     2268
    21632269}
  • opti-behavior/trunk/admin/class-opti-behavior-heatmap-dashboard.php

    r3399182 r3401441  
    908908        }
    909909
    910         // Heatmaps growth (new pages with events this week vs last week)
     910        // Heatmaps growth (this week vs all time)
     911        // Compare: How many NEW pages got heatmaps this week
    911912        $this_week_heatmaps = $wpdb->get_var( $wpdb->prepare(
    912913            "SELECT COUNT(DISTINCT page_id2)
     
    917918        ));
    918919
    919         $last_week_heatmaps = $wpdb->get_var( $wpdb->prepare(
    920             "SELECT COUNT(DISTINCT page_id2)
    921             FROM {$wpdb->prefix}optibehavior_events
    922             WHERE event IN (16, 17, 32, 33, 48, 49)
    923             AND insert_at BETWEEN %s AND %s",
    924             $two_weeks_ago,
    925             $week_ago
    926         ));
    927 
    928         $heatmaps_growth = $last_week_heatmaps > 0 ?
    929             round((($this_week_heatmaps - $last_week_heatmaps) / $last_week_heatmaps) * 100) : 0;
     920        // Calculate: (this week count / all-time count) * 100
     921        // Shows what % of all-time heatmaps were created this week
     922        // Example: 5 this week / 13 all-time = 38% (meaning 38% of all heatmaps are from this week)
     923        $heatmaps_growth = $total_heatmaps > 0 ?
     924            round(($this_week_heatmaps / $total_heatmaps) * 100) : 0;
    930925
    931926        // Total clicks (click events only)
     
    943938        }
    944939
    945         // Clicks growth (this week vs last week)
     940        // Clicks growth (this week vs all time)
    946941        $this_week_clicks = $wpdb->get_var( $wpdb->prepare(
    947942            "SELECT COUNT(*)
     
    952947        ));
    953948
    954         $last_week_clicks = $wpdb->get_var( $wpdb->prepare(
    955             "SELECT COUNT(*)
     949        // Calculate: (this week clicks / all-time clicks) * 100
     950        // Shows what % of all-time clicks happened this week
     951        // Example: 50 this week / 200 all-time = 25% (meaning 25% of all clicks are from this week)
     952        $clicks_growth = $total_clicks > 0 ?
     953            round(($this_week_clicks / $total_clicks) * 100) : 0;
     954
     955        // Mobile percentage - Count PAGES with mobile heatmaps vs total pages (not individual events)
     956        // This should match the data shown in the heatmap table
     957
     958        // Check storage mode to use the same data source as the table
     959        $file_storage = $this->heatmap->get_file_storage();
     960        $storage_settings = $file_storage ? $file_storage->get_settings() : array( 'storage_mode' => 'database' );
     961
     962        if ( $storage_settings['storage_mode'] === 'file' && $file_storage ) {
     963            // File storage mode - get page counts from file storage
     964            $pages_data = $file_storage->get_pages_with_heatmap_data();
     965            $total_pages = count( $pages_data );
     966            $mobile_pages = 0;
     967
     968            foreach ( $pages_data as $page_data ) {
     969                $mobile_interactions = ( $page_data['click_mobile'] + $page_data['breakaway_mobile'] + $page_data['attention_mobile'] );
     970                $total_interactions = ( $page_data['click_pc'] + $page_data['click_mobile'] + $page_data['breakaway_pc'] + $page_data['breakaway_mobile'] + $page_data['attention_pc'] + $page_data['attention_mobile'] );
     971
     972                // Count this page as "mobile" if it has any mobile interactions
     973                if ( $mobile_interactions > 0 ) {
     974                    $mobile_pages++;
     975                }
     976            }
     977
     978            $mobile_percentage = $total_pages > 0 ? round( ( $mobile_pages / $total_pages ) * 100 ) : 0;
     979        } else {
     980            // Database mode - count pages with mobile heatmap data
     981            // A page has "mobile heatmap" if it has any mobile events (17, 33, 49)
     982            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from $wpdb->prefix
     983            $mobile_pages = $wpdb->get_var(
     984                "SELECT COUNT(DISTINCT page_id2)
     985                FROM {$wpdb->prefix}optibehavior_events
     986                WHERE event IN (17, 33, 49)"
     987            );
     988
     989            // Total pages with any heatmap events
     990            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from $wpdb->prefix
     991            $total_pages = $wpdb->get_var(
     992                "SELECT COUNT(DISTINCT page_id2)
     993                FROM {$wpdb->prefix}optibehavior_events
     994                WHERE event IN (16, 17, 32, 33, 48, 49)"
     995            );
     996
     997            $mobile_percentage = $total_pages > 0 ? round( ( $mobile_pages / $total_pages ) * 100 ) : 0;
     998        }
     999
     1000        // Mobile change - compare THIS WEEK vs ALL TIME mobile page percentage
     1001        // This shows how this week's mobile traffic compares to the overall average
     1002        // Calculate this week's mobile percentage
     1003        $this_week_mobile_pages = $wpdb->get_var( $wpdb->prepare(
     1004            "SELECT COUNT(DISTINCT page_id2)
    9561005            FROM {$wpdb->prefix}optibehavior_events
    957             WHERE event IN (16, 17)
    958             AND insert_at BETWEEN %s AND %s",
    959             $two_weeks_ago,
    960             $week_ago
    961         ));
    962 
    963         $clicks_growth = $last_week_clicks > 0 ?
    964             round((($this_week_clicks - $last_week_clicks) / $last_week_clicks) * 100) : 0;
    965 
    966         // Mobile percentage (event 17 = mobile clicks, event 16 = PC clicks)
    967         // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from $wpdb->prefix, event number is hardcoded integer
    968         $mobile_clicks = $wpdb->get_var(
    969             "SELECT COUNT(*)
    970             FROM {$wpdb->prefix}optibehavior_events
    971             WHERE event = 17"
    972         );
    973 
    974         // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from $wpdb->prefix, event number is hardcoded integer
    975         $pc_clicks = $wpdb->get_var(
    976             "SELECT COUNT(*)
    977             FROM {$wpdb->prefix}optibehavior_events
    978             WHERE event = 16"
    979         );
    980 
    981         $total_device_clicks = $mobile_clicks + $pc_clicks;
    982         $mobile_percentage = $total_device_clicks > 0 ?
    983             round(($mobile_clicks / $total_device_clicks) * 100) : 0;
    984 
    985         // Mobile change (this week vs last week)
    986         $this_week_mobile = $wpdb->get_var( $wpdb->prepare(
    987             "SELECT COUNT(*)
    988             FROM {$wpdb->prefix}optibehavior_events
    989             WHERE event = 17
     1006            WHERE event IN (17, 33, 49)
    9901007            AND insert_at >= %s",
    9911008            $week_ago
    9921009        ));
    9931010
    994         $this_week_total = $wpdb->get_var( $wpdb->prepare(
    995             "SELECT COUNT(*)
     1011        $this_week_total_pages = $wpdb->get_var( $wpdb->prepare(
     1012            "SELECT COUNT(DISTINCT page_id2)
    9961013            FROM {$wpdb->prefix}optibehavior_events
    997             WHERE event IN (16, 17)
     1014            WHERE event IN (16, 17, 32, 33, 48, 49)
    9981015            AND insert_at >= %s",
    9991016            $week_ago
    10001017        ));
    10011018
    1002         $this_week_mobile_pct = $this_week_total > 0 ?
    1003             round(($this_week_mobile / $this_week_total) * 100) : 0;
    1004 
     1019        $this_week_mobile_pct = $this_week_total_pages > 0 ? round( ( $this_week_mobile_pages / $this_week_total_pages ) * 100 ) : 0;
     1020
     1021        // Calculate the change: this week's mobile % compared to all-time mobile %
     1022        // Positive means this week has MORE mobile traffic than overall average
     1023        // Negative means this week has LESS mobile traffic than overall average
    10051024        $mobile_change = $this_week_mobile_pct - $mobile_percentage;
    10061025
     
    10141033        $avg_time = $avg_time ? intval($avg_time) : 0;
    10151034
    1016         // Time improvement (this week vs last week)
    1017         // Calculate total duration / total page views for each period
     1035        // Time improvement (this week vs all time average)
    10181036        $this_week_avg_time = $wpdb->get_var( $wpdb->prepare(
    10191037            "SELECT ROUND(SUM(duration) / SUM(page_views))
     
    10241042        ));
    10251043
    1026         $last_week_avg_time = $wpdb->get_var( $wpdb->prepare(
    1027             "SELECT ROUND(SUM(duration) / SUM(page_views))
    1028             FROM {$wpdb->prefix}optibehavior_sessions
    1029             WHERE duration > 0 AND page_views > 0
    1030             AND start_time BETWEEN %s AND %s",
    1031             $two_weeks_ago,
    1032             $week_ago
    1033         ));
    1034 
    1035         $time_improvement = ($last_week_avg_time > 0 && $this_week_avg_time > 0) ?
    1036             round((($this_week_avg_time - $last_week_avg_time) / $last_week_avg_time) * 100) : 0;
     1044        // Compare this week's avg time to all-time avg time
     1045        $time_improvement = ($avg_time > 0 && $this_week_avg_time > 0) ?
     1046            round((($this_week_avg_time - $avg_time) / $avg_time) * 100) : 0;
    10371047
    10381048        // Hottest page (page with most clicks)
     
    10501060        $hottest_page_name = $hottest_page ? $hottest_page->title : 'No data';
    10511061
    1052         // Conversion rate (pages with clicks vs total pageviews)
    1053         $pages_with_clicks = $wpdb->get_var(
    1054             "SELECT COUNT(DISTINCT page_id2)
     1062        // Click-through Rate (CTR) - all time
     1063        // CTR = (total clicks / total pageviews) * 100
     1064        // This shows what % of pageviews resulted in a click
     1065        $total_ctr_clicks = $wpdb->get_var(
     1066            "SELECT COUNT(*)
    10551067            FROM {$wpdb->prefix}optibehavior_events
    10561068            WHERE event IN (16, 17)"
     
    10631075
    10641076        $conversion_rate = $total_pageviews > 0 ?
    1065             round(($pages_with_clicks / $total_pageviews) * 100, 1) : 0;
    1066 
    1067         // CTR improvement (this month vs last month)
    1068         $this_month_clicks = $wpdb->get_var( $wpdb->prepare(
     1077            round(($total_ctr_clicks / $total_pageviews) * 100, 1) : 0;
     1078
     1079        // CTR improvement (this week vs all time)
     1080        $this_week_ctr_clicks = $wpdb->get_var( $wpdb->prepare(
    10691081            "SELECT COUNT(*)
    10701082            FROM {$wpdb->prefix}optibehavior_events
    10711083            WHERE event IN (16, 17)
    10721084            AND insert_at >= %s",
    1073             $month_ago
     1085            $week_ago
    10741086        ));
    10751087
    1076         $this_month_views = $wpdb->get_var( $wpdb->prepare(
     1088        $this_week_views = $wpdb->get_var( $wpdb->prepare(
    10771089            "SELECT COUNT(*)
    10781090            FROM {$wpdb->prefix}optibehavior_pageviews
    10791091            WHERE view_time >= %s",
    1080             $month_ago
     1092            $week_ago
    10811093        ));
    10821094
    1083         $this_month_ctr = $this_month_views > 0 ?
    1084             ($this_month_clicks / $this_month_views) * 100 : 0;
    1085 
     1095        $this_week_ctr = $this_week_views > 0 ?
     1096            round(($this_week_ctr_clicks / $this_week_views) * 100, 1) : 0;
     1097
     1098        // Compare this week's CTR to all-time CTR
    10861099        $ctr_improvement = $conversion_rate > 0 ?
    1087             round($this_month_ctr - $conversion_rate) : 0;
     1100            round((($this_week_ctr - $conversion_rate) / $conversion_rate) * 100) : 0;
    10881101
    10891102        return array(
     
    13651378            $allowed_map = array(
    13661379                'interactions' => 'interactions',
    1367                 'sessions'     => 'sessions',
     1380                'sessions'     => 'COALESCE(s.sessions,0)',
    13681381                'last_updated' => 'last_event_time'
    13691382            );
     
    14911504            // else branch begins
    14921505                $pages_table = $wpdb->prefix . 'optibehavior_pages';
     1506                $events_table = $wpdb->prefix . 'optibehavior_events';
    14931507                $pageviews_table = $wpdb->prefix . 'optibehavior_pageviews';
    14941508                // Aggregated current slice with sessions via subquery (leverages url index)
    14951509                $date_filter_events = ($start_date && $end_date) ? " AND e.insert_at BETWEEN %s AND %s" : '';
    14961510                $date_filter_pv     = ($start_date && $end_date) ? " WHERE pv.view_time BETWEEN %s AND %s" : '';
    1497                 $select_sessions = ($order_by_sql === 'sessions') ? ", COALESCE(s.sessions,0) AS sessions" : "";
    1498                 $join_sessions = ($order_by_sql === 'sessions') ? " LEFT JOIN (SELECT pv.page_id, COUNT(DISTINCT pv.session_id) AS sessions FROM {$pageviews_table} pv{$date_filter_pv} GROUP BY pv.page_id) s ON s.page_id = p.id" : "";
     1511                // Always include sessions in the query, not just when ordering by sessions
     1512            $select_sessions = ", COALESCE(s.sessions,0) AS sessions";
     1513                $join_sessions = " LEFT JOIN (SELECT pv.page_id, COUNT(DISTINCT pv.session_id) AS sessions FROM {$pageviews_table} pv{$date_filter_pv} GROUP BY pv.page_id) s ON s.page_id = p.id";
    14991514                $sql =
    15001515                "SELECT p.id, p.title, p.url,
     
    15151530            $params = array();
    15161531            if ($start_date && $end_date) { $params[] = $start_date; $params[] = $end_date; }
    1517             if ($order_by_sql === 'sessions' && $start_date && $end_date) { $params[] = $start_date; $params[] = $end_date; }
     1532            // Always add date params for sessions join when date filter is active (since we always join now)
     1533            if ($start_date && $end_date) { $params[] = $start_date; $params[] = $end_date; }
    15181534            $params[] = $per_page; $params[] = $offset;
    15191535            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table names are prefixed safely, ORDER BY/direction are validated, all user inputs properly parameterized
     
    15431559                        'url' => $row->url,
    15441560                        'interactions' => (int)$total_interactions,
    1545                         'sessions' => 0, // placeholder; lazy update
     1561                        'sessions' => isset($row->sessions) ? (int)$row->sessions : 0,
    15461562                        'mobile_percentage' => (int)$mobile_percentage,
    15471563                        'last_updated' => $last_updated,
     
    16041620            }
    16051621
     1622            // If sorting by sessions, we need to fetch sessions data first
     1623            $sessions_map_for_sort = array();
     1624            if ( $orderby === 'sessions' ) {
     1625                // Get all page IDs
     1626                $all_page_ids = array_map( function( $page ) {
     1627                    return $page['page_id'];
     1628                }, $pages_data );
     1629
     1630                // Fetch sessions for all pages before sorting
     1631                $sessions_map_for_sort = $this->batch_sessions_counts_by_page( $all_page_ids, $start_date, $end_date );
     1632
     1633                // Add sessions data to pages_data array
     1634                foreach ( $pages_data as &$page_data ) {
     1635                    $page_data['sessions'] = isset( $sessions_map_for_sort[ $page_data['page_id'] ] ) ? (int)$sessions_map_for_sort[ $page_data['page_id'] ] : 0;
     1636                }
     1637                unset( $page_data ); // Break reference
     1638            }
     1639
    16061640            // Sort based on orderby parameter
    16071641            $order_lower = strtolower( $order );
     
    16131647
    16141648                switch ( $orderby ) {
    1615                     case 'clicks':
     1649                    case 'interactions':
     1650                    // Sum all interaction types
     1651                    $val_a = ( $a['click_pc'] + $a['click_mobile'] + $a['breakaway_pc'] + $a['breakaway_mobile'] + $a['attention_pc'] + $a['attention_mobile'] );
     1652                    $val_b = ( $b['click_pc'] + $b['click_mobile'] + $b['breakaway_pc'] + $b['breakaway_mobile'] + $b['attention_pc'] + $b['attention_mobile'] );
     1653                    break;
     1654                case 'sessions':
     1655                    $val_a = isset( $a['sessions'] ) ? (int)$a['sessions'] : 0;
     1656                    $val_b = isset( $b['sessions'] ) ? (int)$b['sessions'] : 0;
     1657                    break;
     1658                case 'clicks':
    16161659                        $val_a = ( $a['click_pc'] + $a['click_mobile'] );
    16171660                        $val_b = ( $b['click_pc'] + $b['click_mobile'] );
     
    17951838
    17961839                $rows[] = array(
    1797                     'page_id' => $page_id,
     1840                    'id' => $page_id,  // Use 'id' to match template expectations
     1841                'page_id' => $page_id,
    17981842                    'title' => $title,
    17991843                    'url' => $url,
     
    18011845                    'type_icon' => $type_icon,
    18021846                    'clicks' => $total_clicks,
    1803                     'interactions' => $total_clicks, // Total interactions (same as clicks for now)
     1847                    'interactions' => $total_clicks + $total_breakaway + $total_attention, // Sum ALL interaction types
    18041848                    'sessions' => $session_count,
    18051849                    'status' => $status,
     
    18881932        $two_weeks_ago = gmdate('Y-m-d', strtotime('-14 days'));
    18891933
    1890         // Top performing pages by clicks
     1934        // Top performing pages by pageviews (more reliable than clicks)
     1935        // Try pageviews first
    18911936        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Static query with prepare
    18921937        $top_pages_query = "SELECT
    1893             p.title,
    1894             p.url,
    1895             COUNT(*) as clicks,
    1896             COUNT(CASE WHEN e.insert_at >= %s THEN 1 END) as recent_clicks,
    1897             COUNT(CASE WHEN e.insert_at BETWEEN %s AND %s THEN 1 END) as previous_clicks
    1898         FROM {$wpdb->prefix}optibehavior_events e
    1899         LEFT JOIN {$wpdb->prefix}optibehavior_pages p ON e.page_id2 = p.id
    1900         WHERE e.event IN (16, 17)
    1901         GROUP BY e.page_id2
    1902         ORDER BY clicks DESC
     1938            COALESCE(NULLIF(pv.title, ''), 'Untitled Page') as title,
     1939            pv.url,
     1940            COUNT(DISTINCT pv.session_id) as views,
     1941            COUNT(DISTINCT CASE WHEN pv.view_time >= %s THEN pv.session_id END) as recent_views,
     1942            COUNT(DISTINCT CASE WHEN pv.view_time BETWEEN %s AND %s THEN pv.session_id END) as previous_views
     1943        FROM {$wpdb->prefix}optibehavior_pageviews pv
     1944        WHERE pv.view_time IS NOT NULL AND pv.url IS NOT NULL AND pv.url != ''
     1945        GROUP BY pv.url
     1946        ORDER BY views DESC
    19031947        LIMIT 5";
    19041948
     
    19111955        ));
    19121956
     1957        // If no pageviews data, fallback to events data
     1958        if ( empty( $top_pages_results ) ) {
     1959            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Static query
     1960            $top_pages_query_fallback = "SELECT
     1961                COALESCE(NULLIF(p.title, ''), 'Untitled Page') as title,
     1962                p.url,
     1963                COUNT(*) as views,
     1964                0 as recent_views,
     1965                0 as previous_views
     1966            FROM {$wpdb->prefix}optibehavior_events e
     1967            JOIN {$wpdb->prefix}optibehavior_pages p ON e.page_id2 = p.id
     1968            WHERE p.url IS NOT NULL AND p.url != ''
     1969            GROUP BY p.url
     1970            ORDER BY views DESC
     1971            LIMIT 5";
     1972
     1973            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Static query
     1974            $top_pages_results = $wpdb->get_results( $top_pages_query_fallback );
     1975        }
     1976
    19131977        $top_pages = array();
    19141978        foreach ($top_pages_results as $page) {
     
    19181982            $change = 0;
    19191983
    1920             if ($page->previous_clicks > 0) {
    1921                 $change = round((($page->recent_clicks - $page->previous_clicks) / $page->previous_clicks) * 100);
     1984            if ($page->previous_views > 0) {
     1985                $change = round((($page->recent_views - $page->previous_views) / $page->previous_views) * 100);
    19221986                if ($change > 0) {
    19231987                    $trend = 'positive';
     
    19271991                    $trend_icon = '📉';
    19281992                }
    1929             } elseif ($page->recent_clicks > 0) {
     1993            } elseif ($page->recent_views > 0) {
    19301994                $trend = 'positive';
    19311995                $trend_icon = '📈';
     
    19412005                'title' => $title,
    19422006                'url' => $page->url,
    1943                 'clicks' => intval($page->clicks),
     2007                'clicks' => intval($page->views),
    19442008                'trend' => $trend,
    19452009                'trend_icon' => $trend_icon,
     
    25082572        );
    25092573
    2510         // If no events data, fall back to session pages (page views).
     2574        // If no events data, fall back to pageviews table (page views).
    25112575        if ( empty( $results ) ) {
    2512             // Get top pages from session_pages table (page views).
     2576            // Get top pages from pageviews table (page views).
    25132577            // phpcs:ignore WordPress.DB.DirectDatabaseQuery
    25142578            $results = $wpdb->get_results(
    25152579                $wpdb->prepare(
    25162580                    "SELECT p.id as page_id, p.url, p.title, COUNT(*) as interactions
    2517                     FROM {$wpdb->prefix}optibehavior_session_pages sp
    2518                     LEFT JOIN {$wpdb->prefix}optibehavior_pages p ON sp.page_id = p.id
    2519                     WHERE sp.visited_at BETWEEN %s AND %s
     2581                    FROM {$wpdb->prefix}optibehavior_pageviews pv
     2582                    LEFT JOIN {$wpdb->prefix}optibehavior_pages p ON pv.page_id = p.id
     2583                    WHERE pv.view_time BETWEEN %s AND %s
    25202584                    GROUP BY p.id
    25212585                    ORDER BY interactions DESC
     
    25312595                $results = $wpdb->get_results(
    25322596                    "SELECT p.id as page_id, p.url, p.title, COUNT(*) as interactions
    2533                     FROM {$wpdb->prefix}optibehavior_session_pages sp
    2534                     LEFT JOIN {$wpdb->prefix}optibehavior_pages p ON sp.page_id = p.id
     2597                    FROM {$wpdb->prefix}optibehavior_pageviews pv
     2598                    LEFT JOIN {$wpdb->prefix}optibehavior_pages p ON pv.page_id = p.id
    25352599                    GROUP BY p.id
    25362600                    ORDER BY interactions DESC
     
    25402604        }
    25412605
     2606        // Final fallback: try events table without date filter if we still have no results.
     2607        // Use all events, not just specific types, to ensure we capture all data.
     2608        if ( empty( $results ) ) {
     2609            // phpcs:ignore WordPress.DB.DirectDatabaseQuery
     2610            $results = $wpdb->get_results(
     2611                "SELECT p.id as page_id, p.url, p.title, COUNT(*) as interactions
     2612                FROM {$wpdb->prefix}optibehavior_events e
     2613                LEFT JOIN {$wpdb->prefix}optibehavior_pages p ON e.page_id2 = p.id
     2614                WHERE p.id IS NOT NULL
     2615                GROUP BY p.id
     2616                ORDER BY interactions DESC
     2617                LIMIT 5"
     2618            );
     2619        }
     2620
    25422621        if ( empty( $results ) ) {
    25432622            return array();
     
    25682647
    25692648            // Get click counts (events 16,17) for current and previous periods for the same pages.
     2649            // Try page_id2 first, then fall back to page_id for compatibility
    25702650            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
    25712651            $sql = "
    2572                 SELECT e.page_id2 AS page_id,
    2573                     SUM( CASE WHEN e.insert_at BETWEEN %s AND %s THEN 1 ELSE 0 END ) AS current_clicks,
    2574                     SUM( CASE WHEN e.insert_at BETWEEN %s AND %s THEN 1 ELSE 0 END ) AS previous_clicks
     2652                SELECT COALESCE(e.page_id2, e.page_id) AS page_id,
     2653                    COUNT(*) AS current_clicks
    25752654                FROM {$wpdb->prefix}optibehavior_events e
    25762655                WHERE e.event IN (16,17)
    2577                     AND e.page_id2 IN ( $placeholders )
    2578                 GROUP BY e.page_id2
     2656                    AND (e.page_id2 IN ( $placeholders ) OR e.page_id IN ( $placeholders ))
     2657                GROUP BY COALESCE(e.page_id2, e.page_id)
    25792658            ";
    25802659
    2581             $params   = array( $start_date, $end_date, $prev_start, $prev_end );
    2582             $params   = array_merge( $params, $page_ids );
     2660            // No date params needed for all-time click count
     2661            $params   = array_merge( $page_ids, $page_ids );
    25832662
    25842663            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
     
    25872666                $page_id  = intval( $row->page_id );
    25882667                $current   = intval( $row->current_clicks );
    2589                 $previous  = intval( $row->previous_clicks );
     2668                $previous  = 0; // No previous period comparison for all-time clicks
    25902669                $click_stats[ $page_id ] = array(
    25912670                    'current'  => $current,
     
    25962675
    25972676            // Get interactions for previous period for trend on heatmap views.
     2677            // Try page_id2 first, then fall back to page_id for compatibility
    25982678            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
    25992679            $sql_prev = "
    2600                 SELECT e.page_id2 AS page_id,
     2680                SELECT COALESCE(e.page_id2, e.page_id) AS page_id,
    26012681                    COUNT(*) AS previous_views
    26022682                FROM {$wpdb->prefix}optibehavior_events e
    26032683                WHERE e.insert_at BETWEEN %s AND %s
    26042684                    AND e.event IN (16,17,32,33,48,49)
    2605                     AND e.page_id2 IN ( $placeholders )
    2606                 GROUP BY e.page_id2
     2685                    AND (e.page_id2 IN ( $placeholders ) OR e.page_id IN ( $placeholders ))
     2686                GROUP BY COALESCE(e.page_id2, e.page_id)
    26072687            ";
    26082688
    26092689            $params_prev   = array( $prev_start, $prev_end );
    2610             $params_prev   = array_merge( $params_prev, $page_ids );
     2690            $params_prev   = array_merge( $params_prev, $page_ids, $page_ids );
    26112691
    26122692            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
  • opti-behavior/trunk/admin/class-opti-behavior-heatmap-post-metabox.php

    r3399182 r3401441  
    5454        add_action( 'wp_ajax_optibehavior_post_analytics', array( $this, 'ajax_post_analytics' ) );
    5555        add_action( 'wp_ajax_optibehavior_post_timeseries', array( $this, 'ajax_post_timeseries' ) );
     56        add_action( 'wp_ajax_optibehavior_post_analytics_chart', array( $this, 'ajax_post_analytics_chart' ) );
    5657        add_action( 'wp_ajax_optibehavior_post_referrers', array( $this, 'ajax_post_referrers' ) );
    5758        add_action( 'wp_ajax_optibehavior_post_outbound_clicks', array( $this, 'ajax_post_outbound_clicks' ) );
     59        add_action( 'wp_ajax_optibehavior_post_countries', array( $this, 'ajax_post_countries' ) );
     60        add_action( 'wp_ajax_optibehavior_post_browsers', array( $this, 'ajax_post_browsers' ) );
     61        add_action( 'wp_ajax_optibehavior_post_devices', array( $this, 'ajax_post_devices' ) );
    5862    }
    5963
     
    7074        add_meta_box(
    7175            'optibehavior_editor_analytics',
    72             '<i data-lucide="flame" style="width:16px;height:16px;vertical-align:middle;margin-right:4px;"></i>' . __( 'Heatmap Analytics', 'opti-behavior' ),
     76            '<i data-lucide="flame" style="width:16px;height:16px;vertical-align:middle;margin-right:4px;"></i>' . __( 'Post Analytics', 'opti-behavior' ),
    7377            array( $this, 'render_editor_analytics_box' ),
    7478            array( 'post', 'page' ),
     
    206210                        <div class="optibehavior-behavior-stats">
    207211                            <div class="optibehavior-behavior-stat">
    208                                 <div class="optibehavior-behavior-label"><?php echo esc_html__( 'Avg Time on Page', 'opti-behavior' ); ?></div>
    209                                 <div class="optibehavior-behavior-value" id="optibehavior-avg">—</div>
     212                                <div class="optibehavior-behavior-label">
     213                                    <i data-lucide="clock" style="width:14px;height:14px;margin-right:4px;vertical-align:middle;"></i>
     214                                    <?php echo esc_html__( 'Avg Time on Page', 'opti-behavior' ); ?>
     215                                </div>
     216                                <div class="optibehavior-behavior-value">
     217                                    <span class="optibehavior-stat-badge" id="optibehavior-avg">—</span>
     218                                </div>
    210219                            </div>
    211220                            <div class="optibehavior-behavior-stat">
    212                                 <div class="optibehavior-behavior-label"><?php echo esc_html__( 'Total Interactions', 'opti-behavior' ); ?></div>
    213                                 <div class="optibehavior-behavior-value" id="optibehavior-int">—</div>
     221                                <div class="optibehavior-behavior-label">
     222                                    <i data-lucide="mouse-pointer-click" style="width:14px;height:14px;margin-right:4px;vertical-align:middle;"></i>
     223                                    <?php echo esc_html__( 'Total Interactions', 'opti-behavior' ); ?>
     224                                </div>
     225                                <div class="optibehavior-behavior-value">
     226                                    <span class="optibehavior-stat-badge" id="optibehavior-int">—</span>
     227                                </div>
    214228                            </div>
    215229                            <div class="optibehavior-behavior-stat">
    216                                 <div class="optibehavior-behavior-label"><?php echo esc_html__( 'Device Split', 'opti-behavior' ); ?></div>
     230                                <div class="optibehavior-behavior-label">
     231                                    <i data-lucide="arrow-down" style="width:14px;height:14px;margin-right:4px;vertical-align:middle;"></i>
     232                                    <?php echo esc_html__( 'Avg. Scroll Depth', 'opti-behavior' ); ?>
     233                                </div>
     234                                <div class="optibehavior-behavior-value">
     235                                    <span class="optibehavior-stat-badge" id="optibehavior-scroll">—</span>
     236                                </div>
     237                            </div>
     238                            <div class="optibehavior-behavior-stat">
     239                                <div class="optibehavior-behavior-label">
     240                                    <i data-lucide="log-out" style="width:14px;height:14px;margin-right:4px;vertical-align:middle;"></i>
     241                                    <?php echo esc_html__( 'Bounce Rate', 'opti-behavior' ); ?>
     242                                </div>
     243                                <div class="optibehavior-behavior-value">
     244                                    <span class="optibehavior-stat-badge" id="optibehavior-bounce">—</span>
     245                                </div>
     246                            </div>
     247                            <div class="optibehavior-behavior-stat">
     248                                <div class="optibehavior-behavior-label">
     249                                    <i data-lucide="monitor-smartphone" style="width:14px;height:14px;margin-right:4px;vertical-align:middle;"></i>
     250                                    <?php echo esc_html__( 'Device Split', 'opti-behavior' ); ?>
     251                                </div>
    217252                                <div class="optibehavior-behavior-value" id="optibehavior-split">—</div>
    218253                            </div>
    219254                            <div class="optibehavior-behavior-stat">
    220                                 <div class="optibehavior-behavior-label"><?php echo esc_html__( 'Last Updated', 'opti-behavior' ); ?></div>
     255                                <div class="optibehavior-behavior-label">
     256                                    <i data-lucide="refresh-cw" style="width:14px;height:14px;margin-right:4px;vertical-align:middle;"></i>
     257                                    <?php echo esc_html__( 'Last Updated', 'opti-behavior' ); ?>
     258                                </div>
    221259                                <div class="optibehavior-behavior-value" id="optibehavior-upd">—</div>
    222260                            </div>
     
    242280            </div>
    243281
     282            <!-- New Analytics Row: Countries, Browsers, Device Types -->
     283            <div class="optibehavior-analytics-grid optibehavior-secondary-grid">
     284                <!-- Countries Column -->
     285                <div class="optibehavior-analytics-column optibehavior-countries-column">
     286                    <div class="optibehavior-column-header">
     287                        <div class="optibehavior-column-icon"><i data-lucide="globe" style="width:18px;height:18px;"></i></div>
     288                        <h3 class="optibehavior-column-title"><?php echo esc_html__( 'Countries', 'opti-behavior' ); ?></h3>
     289                    </div>
     290                    <div class="optibehavior-column-content">
     291                        <div id="optibehavior-countries-content">
     292                            <div id="optibehavior-countries-table" class="optibehavior-scroll-area"></div>
     293                            <div id="optibehavior-countries-empty" class="optibehavior-empty-state">
     294                                <i data-lucide="globe" style="width: 48px; height: 48px; color: #9ca3af; stroke-width: 1.5;"></i>
     295                                <div class="optibehavior-empty-title"><?php echo esc_html__( 'No country data available', 'opti-behavior' ); ?></div>
     296                                <div class="optibehavior-empty-sub"><?php echo esc_html__( 'Data will appear once visitors access this page.', 'opti-behavior' ); ?></div>
     297                            </div>
     298                        </div>
     299                    </div>
     300                </div>
     301
     302                <!-- Browsers Column -->
     303                <div class="optibehavior-analytics-column optibehavior-browsers-column">
     304                    <div class="optibehavior-column-header">
     305                        <div class="optibehavior-column-icon"><i data-lucide="compass" style="width:18px;height:18px;"></i></div>
     306                        <h3 class="optibehavior-column-title"><?php echo esc_html__( 'Browsers', 'opti-behavior' ); ?></h3>
     307                    </div>
     308                    <div class="optibehavior-column-content">
     309                        <div id="optibehavior-browsers-content">
     310                            <div id="optibehavior-browsers-table" class="optibehavior-scroll-area"></div>
     311                            <div id="optibehavior-browsers-empty" class="optibehavior-empty-state">
     312                                <i data-lucide="compass" style="width: 48px; height: 48px; color: #9ca3af; stroke-width: 1.5;"></i>
     313                                <div class="optibehavior-empty-title"><?php echo esc_html__( 'No browser data available', 'opti-behavior' ); ?></div>
     314                                <div class="optibehavior-empty-sub"><?php echo esc_html__( 'Data will appear once visitors access this page.', 'opti-behavior' ); ?></div>
     315                            </div>
     316                        </div>
     317                    </div>
     318                </div>
     319
     320                <!-- Device Types Column -->
     321                <div class="optibehavior-analytics-column optibehavior-devices-column">
     322                    <div class="optibehavior-column-header">
     323                        <div class="optibehavior-column-icon"><i data-lucide="monitor-smartphone" style="width:18px;height:18px;"></i></div>
     324                        <h3 class="optibehavior-column-title"><?php echo esc_html__( 'Device Types', 'opti-behavior' ); ?></h3>
     325                    </div>
     326                    <div class="optibehavior-column-content">
     327                        <div id="optibehavior-devices-content">
     328                            <div id="optibehavior-devices-table" class="optibehavior-scroll-area"></div>
     329                            <div id="optibehavior-devices-empty" class="optibehavior-empty-state">
     330                                <i data-lucide="monitor-smartphone" style="width: 48px; height: 48px; color: #9ca3af; stroke-width: 1.5;"></i>
     331                                <div class="optibehavior-empty-title"><?php echo esc_html__( 'No device data available', 'opti-behavior' ); ?></div>
     332                                <div class="optibehavior-empty-sub"><?php echo esc_html__( 'Data will appear once visitors access this page.', 'opti-behavior' ); ?></div>
     333                            </div>
     334                        </div>
     335                    </div>
     336                </div>
     337            </div>
     338
    244339            <!-- Controls Row -->
    245340            <div class="optibehavior-controls-row">
     
    257352                    </div>
    258353                </div>
    259                 <canvas id="optibehavior-chart" height="110"></canvas>
    260                 <div id="optibehavior-empty" class="optibehavior-empty"><?php echo esc_html__( 'No heatmap data yet for this page.', 'opti-behavior' ); ?></div>
     354                <div id="optibehavior-chart-loading" class="optibehavior-loading-state" style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px 20px; gap: 12px;">
     355                    <span class="spinner is-active"></span>
     356                    <span style="color: #64748b; font-size: 14px;"><?php echo esc_html__( 'Loading analytics data...', 'opti-behavior' ); ?></span>
     357                </div>
     358                <canvas id="optibehavior-chart" height="280" style="display: none;"></canvas>
     359                <div id="optibehavior-empty" class="optibehavior-empty" style="display: none;"><?php echo esc_html__( 'No heatmap data yet for this page.', 'opti-behavior' ); ?></div>
    261360            </div>
    262361        </div>
     
    271370    public function ajax_post_analytics() {
    272371        if ( ! isset( $_POST['post_id'] ) ) {
    273             wp_send_json_error();
     372            wp_send_json_error( array( 'message' => 'No post_id provided' ) );
    274373        }
    275374
     
    278377
    279378        if ( ! current_user_can( 'edit_post', $post_id ) ) {
    280             wp_send_json_error();
     379            wp_send_json_error( array( 'message' => 'Permission denied' ) );
    281380        }
    282381
     
    284383        $url = get_permalink( $post_id );
    285384        if ( ! $url ) {
    286             wp_send_json_error();
    287         }
    288 
    289         // Get PC clicks
    290         $pc = intval( $wpdb->get_var( $wpdb->prepare(
     385            wp_send_json_error( array( 'message' => 'No permalink found' ) );
     386        }
     387
     388        // Get page_id for this URL (needed for file storage)
     389        $page_id_for_events = $wpdb->get_var( $wpdb->prepare(
     390            "SELECT id FROM {$wpdb->prefix}optibehavior_pages WHERE url=%s LIMIT 1",
     391            $url
     392        ) );
     393
     394        // If exact match not found, try with trailing slash variations
     395        if ( ! $page_id_for_events ) {
     396            $url_alt = rtrim( $url, '/' ) . '/';
     397            if ( $url_alt === $url ) {
     398                $url_alt = rtrim( $url, '/' );
     399            }
     400            $page_id_for_events = $wpdb->get_var( $wpdb->prepare(
     401                "SELECT id FROM {$wpdb->prefix}optibehavior_pages WHERE url=%s LIMIT 1",
     402                $url_alt
     403            ) );
     404        }
     405
     406        // Check storage mode
     407        $file_storage = $this->core->get_file_storage();
     408        $storage_settings = $file_storage ? $file_storage->get_settings() : array( 'storage_mode' => 'database' );
     409        $is_file_storage = ( $storage_settings['storage_mode'] === 'file' && $file_storage && $page_id_for_events );
     410
     411        // Get PC clicks - use URL-based filtering to ensure all events are counted
     412        if ( $is_file_storage && $page_id_for_events ) {
     413            $pc = intval( $file_storage->count_events( $page_id_for_events, 16 ) );
     414        } else {
     415            $pc = 0;
     416        }
     417        // Always verify with database query for consistency
     418        $pc_db = intval( $wpdb->get_var( $wpdb->prepare(
    291419            "SELECT COUNT(*) FROM {$wpdb->prefix}optibehavior_events e JOIN {$wpdb->prefix}optibehavior_pages p ON e.page_id2=p.id WHERE e.event=16 AND (p.url=%s OR p.url LIKE %s)",
    292420            $url, $url . '%' ) ) );
    293 
    294         // Get mobile clicks
    295         $mb = intval( $wpdb->get_var( $wpdb->prepare(
     421        $pc = max( $pc, $pc_db );
     422
     423        // Get mobile clicks - use URL-based filtering to ensure all events are counted
     424        if ( $is_file_storage && $page_id_for_events ) {
     425            $mb = intval( $file_storage->count_events( $page_id_for_events, 17 ) );
     426        } else {
     427            $mb = 0;
     428        }
     429        // Always verify with database query for consistency
     430        $mb_db = intval( $wpdb->get_var( $wpdb->prepare(
    296431            "SELECT COUNT(*) FROM {$wpdb->prefix}optibehavior_events e JOIN {$wpdb->prefix}optibehavior_pages p ON e.page_id2=p.id WHERE e.event=17 AND (p.url=%s OR p.url LIKE %s)",
    297432            $url, $url . '%' ) ) );
     433        $mb = max( $mb, $mb_db );
    298434
    299435        $total = $pc + $mb;
     436        // Note: Outbound clicks are already included in PC/MB event counts (events 16/17)
     437        // The optibehavior_outbound_clicks table tracks WHERE users clicked (which links)
     438        // but the click events themselves are recorded in the events table as event 16 or 17
    300439
    301440        // Get average time on page - try multiple approaches
     
    340479        }
    341480
    342         // Get last updated timestamp
    343         $last_updated = $wpdb->get_var( $wpdb->prepare(
    344             "SELECT MAX(e.insert_at) FROM {$wpdb->prefix}optibehavior_events e JOIN {$wpdb->prefix}optibehavior_pages p ON e.page_id2=p.id WHERE p.url=%s OR p.url LIKE %s",
     481        // Get last updated timestamp from multiple sources
     482        $last_from_events = null;
     483        if ( $is_file_storage ) {
     484            // Get latest event from file storage
     485            $pc_events = $file_storage->read_events( $page_id_for_events, 16, 1 );
     486            $mb_events = $file_storage->read_events( $page_id_for_events, 17, 1 );
     487
     488            $pc_time = ! empty( $pc_events ) && isset( $pc_events[0]['insert_at'] ) ? $pc_events[0]['insert_at'] : null;
     489            $mb_time = ! empty( $mb_events ) && isset( $mb_events[0]['insert_at'] ) ? $mb_events[0]['insert_at'] : null;
     490
     491            if ( $pc_time && $mb_time ) {
     492                $last_from_events = ( strtotime( $pc_time ) > strtotime( $mb_time ) ) ? $pc_time : $mb_time;
     493            } elseif ( $pc_time ) {
     494                $last_from_events = $pc_time;
     495            } elseif ( $mb_time ) {
     496                $last_from_events = $mb_time;
     497            }
     498        } else {
     499            $last_from_events = $wpdb->get_var( $wpdb->prepare(
     500                "SELECT MAX(e.insert_at) FROM {$wpdb->prefix}optibehavior_events e JOIN {$wpdb->prefix}optibehavior_pages p ON e.page_id2=p.id WHERE p.url=%s OR p.url LIKE %s",
     501                $url, $url . '%' ) );
     502        }
     503
     504        $last_from_pageviews = $wpdb->get_var( $wpdb->prepare(
     505            "SELECT MAX(view_time) FROM {$wpdb->prefix}optibehavior_pageviews WHERE url=%s OR url LIKE %s",
    345506            $url, $url . '%' ) );
    346507
     508        $last_from_outbound = null;
     509        if ( $page_id_for_events ) {
     510            $last_from_outbound = $wpdb->get_var( $wpdb->prepare(
     511                "SELECT MAX(created_at) FROM {$wpdb->prefix}optibehavior_outbound_clicks WHERE page_id=%d",
     512                $page_id_for_events
     513            ) );
     514        }
     515
     516        // Get the most recent timestamp from all sources
     517        $timestamps = array_filter( array( $last_from_events, $last_from_pageviews, $last_from_outbound ) );
     518        $last_updated_raw = ! empty( $timestamps ) ? max( $timestamps ) : null;
     519
     520        // Format the timestamp for display
     521        $last_updated = null;
     522        if ( $last_updated_raw ) {
     523            $diff = current_time( 'timestamp' ) - strtotime( $last_updated_raw );
     524            if ( $diff < 60 ) {
     525                $last_updated = __( 'Just now', 'opti-behavior' );
     526            } elseif ( $diff < 3600 ) {
     527                /* translators: %d: number of minutes */
     528                $last_updated = sprintf( __( '%d min ago', 'opti-behavior' ), floor( $diff / 60 ) );
     529            } elseif ( $diff < 86400 ) {
     530                /* translators: %d: number of hours */
     531                $last_updated = sprintf( __( '%d hours ago', 'opti-behavior' ), floor( $diff / 3600 ) );
     532            } else {
     533                /* translators: %d: number of days */
     534                $last_updated = sprintf( __( '%d days ago', 'opti-behavior' ), floor( $diff / 86400 ) );
     535            }
     536        }
     537
    347538        $mobile_pct = $total > 0 ? round( ( $mb / $total ) * 100 ) : 0;
     539
     540        // Get average scroll depth
     541        $avg_scroll_depth = intval( $wpdb->get_var( $wpdb->prepare(
     542            "SELECT AVG(scroll_depth) FROM {$wpdb->prefix}optibehavior_pageviews
     543             WHERE (url=%s OR url LIKE %s) AND scroll_depth > 0",
     544            $url, $url . '%' ) ) );
     545
     546        // Get bounce rate - sessions with only 1 pageview
     547        $total_sessions = intval( $wpdb->get_var( $wpdb->prepare(
     548            "SELECT COUNT(DISTINCT pv.session_id) FROM {$wpdb->prefix}optibehavior_pageviews pv
     549             WHERE (pv.url=%s OR pv.url LIKE %s)",
     550            $url, $url . '%' ) ) );
     551
     552        $bounced_sessions = intval( $wpdb->get_var( $wpdb->prepare(
     553            "SELECT COUNT(DISTINCT pv.session_id) FROM {$wpdb->prefix}optibehavior_pageviews pv
     554             WHERE (pv.url=%s OR pv.url LIKE %s)
     555               AND pv.session_id IN (
     556                   SELECT session_id FROM {$wpdb->prefix}optibehavior_pageviews
     557                   GROUP BY session_id
     558                   HAVING COUNT(*) = 1
     559               )",
     560            $url, $url . '%' ) ) );
     561
     562        $bounce_rate = $total_sessions > 0 ? round( ( $bounced_sessions / $total_sessions ) * 100 ) : 0;
    348563
    349564        // Get page ID for heatmap viewing URLs using the analytics class method
     
    358573            'mobile_clicks' => $mb,
    359574            'avg_time' => $avg_time,
     575            'avg_scroll_depth' => $avg_scroll_depth,
     576            'bounce_rate' => $bounce_rate,
    360577            'mobile_pct' => $mobile_pct,
    361578            'last_updated' => $last_updated,
     
    511728
    512729    /**
     730     * AJAX handler for enhanced post analytics chart data.
     731     * Returns Total Visitors, Desktop Visitors, Desktop Events, Mobile Visitors, Mobile Events.
     732     *
     733     * @since 1.0.3
     734     */
     735    public function ajax_post_analytics_chart() {
     736        if ( ! isset( $_POST['post_id'] ) ) {
     737            wp_send_json_error( array( 'message' => 'No post_id provided' ) );
     738        }
     739
     740        $post_id = intval( $_POST['post_id'] );
     741        check_ajax_referer( 'optibehavior_post_analytics_' . $post_id, 'nonce' );
     742
     743        if ( ! current_user_can( 'edit_post', $post_id ) ) {
     744            wp_send_json_error( array( 'message' => 'Permission denied' ) );
     745        }
     746
     747        global $wpdb;
     748        $url = get_permalink( $post_id );
     749        if ( ! $url ) {
     750            wp_send_json_error( array( 'message' => 'No permalink found' ) );
     751        }
     752
     753        $range = isset( $_POST['range'] ) ? sanitize_text_field( wp_unslash( $_POST['range'] ) ) : '7d';
     754        $start = null;
     755        $group_format = '%Y-%m-%d';
     756
     757        switch ( $range ) {
     758            case '24h':
     759                $start = gmdate( 'Y-m-d H:i:s', current_time( 'timestamp' ) - 24 * 3600 );
     760                $group_format = '%Y-%m-%d %H:00:00';
     761                break;
     762            case '7d':
     763                $start = gmdate( 'Y-m-d H:i:s', current_time( 'timestamp' ) - 7 * 86400 );
     764                break;
     765            case '30d':
     766                $start = gmdate( 'Y-m-d H:i:s', current_time( 'timestamp' ) - 30 * 86400 );
     767                break;
     768            case '90d':
     769                $start = gmdate( 'Y-m-d H:i:s', current_time( 'timestamp' ) - 90 * 86400 );
     770                break;
     771            case 'all':
     772            default:
     773                $start = '1970-01-01 00:00:00';
     774        }
     775
     776        // Get page_id2 first for all queries
     777        $page_id2 = intval( $wpdb->get_var( $wpdb->prepare(
     778            "SELECT id FROM {$wpdb->prefix}optibehavior_pages WHERE url=%s LIMIT 1",
     779            $url
     780        ) ) );
     781
     782        // If not found, try with/without trailing slash
     783        if ( ! $page_id2 ) {
     784            $url_alt = rtrim( $url, '/' ) . '/';
     785            if ( $url_alt === $url ) {
     786                $url_alt = rtrim( $url, '/' );
     787            }
     788            $page_id2 = intval( $wpdb->get_var( $wpdb->prepare(
     789                "SELECT id FROM {$wpdb->prefix}optibehavior_pages WHERE url=%s LIMIT 1",
     790                $url_alt
     791            ) ) );
     792        }
     793
     794        // If still not found, try URL LIKE match
     795        if ( ! $page_id2 ) {
     796            $page_id2 = intval( $wpdb->get_var( $wpdb->prepare(
     797                "SELECT id FROM {$wpdb->prefix}optibehavior_pages WHERE url LIKE %s LIMIT 1",
     798                $url . '%'
     799            ) ) );
     800        }
     801
     802        // Get unique visitors and sessions (Total, Desktop and Mobile) over time
     803        // We need to get data from both pageviews AND events to ensure consistency
     804        if ( $group_format === '%Y-%m-%d %H:00:00' ) {
     805            // Get visitors from pageviews
     806            $pv_visitors_data = $wpdb->get_results(
     807                $wpdb->prepare(
     808                    "SELECT DATE_FORMAT(pv.view_time, '%%Y-%%m-%%d %%H:00:00') AS bucket,
     809                        COUNT(DISTINCT pv.session_id) AS total_sessions,
     810                        COUNT(DISTINCT CASE WHEN v.device_type = 'Desktop' THEN pv.session_id END) AS desktop_sessions,
     811                        COUNT(DISTINCT CASE WHEN v.device_type = 'Mobile' THEN pv.session_id END) AS mobile_sessions
     812                     FROM {$wpdb->prefix}optibehavior_pageviews pv
     813                     LEFT JOIN {$wpdb->prefix}optibehavior_visitors v ON pv.visitor_id = v.id
     814                     WHERE (pv.url=%s OR pv.url LIKE %s) AND pv.view_time >= %s
     815                     GROUP BY bucket
     816                     ORDER BY bucket ASC",
     817                    $url, $url . '%', $start
     818                ),
     819                ARRAY_A
     820            );
     821
     822            // Get visitors from events (for cases where pageview doesn't exist but events do)
     823            // Use the SAME approach as the working stats query - join with pages table by URL
     824            $ev_visitors_data = $wpdb->get_results(
     825                $wpdb->prepare(
     826                    "SELECT DATE_FORMAT(e.insert_at, '%%Y-%%m-%%d %%H:00:00') AS bucket,
     827                        COUNT(DISTINCT e.session_id) AS total_sessions,
     828                        COUNT(DISTINCT CASE WHEN v.device_type = 'Desktop' THEN e.session_id END) AS desktop_sessions,
     829                        COUNT(DISTINCT CASE WHEN v.device_type = 'Mobile' THEN e.session_id END) AS mobile_sessions
     830                     FROM {$wpdb->prefix}optibehavior_events e
     831                     JOIN {$wpdb->prefix}optibehavior_pages p ON e.page_id2=p.id
     832                     LEFT JOIN {$wpdb->prefix}optibehavior_sessions s ON e.session_id = s.id
     833                     LEFT JOIN {$wpdb->prefix}optibehavior_visitors v ON s.visitor_id = v.id
     834                     WHERE (p.url=%s OR p.url LIKE %s) AND e.insert_at >= %s
     835                     GROUP BY bucket
     836                     ORDER BY bucket ASC",
     837                    $url, $url . '%', $start
     838                ),
     839                ARRAY_A
     840            );
     841        } else {
     842            // Get visitors from pageviews
     843            $pv_visitors_data = $wpdb->get_results(
     844                $wpdb->prepare(
     845                    "SELECT DATE_FORMAT(pv.view_time, '%%Y-%%m-%%d') AS bucket,
     846                        COUNT(DISTINCT pv.session_id) AS total_sessions,
     847                        COUNT(DISTINCT CASE WHEN v.device_type = 'Desktop' THEN pv.session_id END) AS desktop_sessions,
     848                        COUNT(DISTINCT CASE WHEN v.device_type = 'Mobile' THEN pv.session_id END) AS mobile_sessions
     849                     FROM {$wpdb->prefix}optibehavior_pageviews pv
     850                     LEFT JOIN {$wpdb->prefix}optibehavior_visitors v ON pv.visitor_id = v.id
     851                     WHERE (pv.url=%s OR pv.url LIKE %s) AND pv.view_time >= %s
     852                     GROUP BY bucket
     853                     ORDER BY bucket ASC",
     854                    $url, $url . '%', $start
     855                ),
     856                ARRAY_A
     857            );
     858
     859            // Get visitors from events (for cases where pageview doesn't exist but events do)
     860            $ev_visitors_data = array();
     861            if ( $page_id2 > 0 ) {
     862                $ev_visitors_data = $wpdb->get_results(
     863                    $wpdb->prepare(
     864                        "SELECT DATE_FORMAT(e.insert_at, '%%Y-%%m-%%d') AS bucket,
     865                            COUNT(DISTINCT e.session_id) AS total_sessions,
     866                            COUNT(DISTINCT CASE WHEN v.device_type = 'Desktop' THEN e.session_id END) AS desktop_sessions,
     867                            COUNT(DISTINCT CASE WHEN v.device_type = 'Mobile' THEN e.session_id END) AS mobile_sessions
     868                         FROM {$wpdb->prefix}optibehavior_events e
     869                         LEFT JOIN {$wpdb->prefix}optibehavior_sessions s ON e.session_id = s.id
     870                         LEFT JOIN {$wpdb->prefix}optibehavior_visitors v ON s.visitor_id = v.id
     871                         WHERE e.page_id2=%d AND e.insert_at >= %s
     872                         GROUP BY bucket
     873                         ORDER BY bucket ASC",
     874                        $page_id2, $start
     875                    ),
     876                    ARRAY_A
     877                );
     878            }
     879        }
     880
     881        // Merge pageview and event visitor data to get the maximum count
     882        $visitors_data_merged = array();
     883        foreach ( $pv_visitors_data as $row ) {
     884            $bucket = $row['bucket'];
     885            $visitors_data_merged[ $bucket ] = $row;
     886        }
     887        foreach ( $ev_visitors_data as $row ) {
     888            $bucket = $row['bucket'];
     889            if ( ! isset( $visitors_data_merged[ $bucket ] ) ) {
     890                $visitors_data_merged[ $bucket ] = $row;
     891            } else {
     892                // Take the maximum of both sources
     893                $visitors_data_merged[ $bucket ]['total_sessions'] = max(
     894                    intval( $visitors_data_merged[ $bucket ]['total_sessions'] ),
     895                    intval( $row['total_sessions'] )
     896                );
     897                $visitors_data_merged[ $bucket ]['desktop_sessions'] = max(
     898                    intval( $visitors_data_merged[ $bucket ]['desktop_sessions'] ),
     899                    intval( $row['desktop_sessions'] )
     900                );
     901                $visitors_data_merged[ $bucket ]['mobile_sessions'] = max(
     902                    intval( $visitors_data_merged[ $bucket ]['mobile_sessions'] ),
     903                    intval( $row['mobile_sessions'] )
     904                );
     905            }
     906        }
     907        $visitors_data = array_values( $visitors_data_merged );
     908
     909        // Check storage mode for events
     910        $file_storage      = $this->core->get_file_storage();
     911        $storage_settings  = $file_storage ? $file_storage->get_settings() : array( 'storage_mode' => 'database' );
     912        $is_file_storage   = ( $storage_settings['storage_mode'] === 'file' && $file_storage && $page_id2 );
     913
     914        // Get events (Desktop=16, Mobile=17) over time
     915        $events_data = array();
     916        if ( $page_id2 > 0 ) {
     917            if ( $is_file_storage ) {
     918                // Get events from file storage - need to read files and group by time buckets
     919                // Get all events from file storage using read_events (not get_events)
     920                // Parameters: page_id, event_id, limit (0=all), start_date, end_date
     921                $all_events_desktop = $file_storage->read_events( $page_id2, 16, 0, gmdate( 'Y-m-d', strtotime( $start ) ), gmdate( 'Y-m-d' ) );
     922                $all_events_mobile  = $file_storage->read_events( $page_id2, 17, 0, gmdate( 'Y-m-d', strtotime( $start ) ), gmdate( 'Y-m-d' ) );
     923
     924                // Group events by time buckets
     925                $events_by_bucket = array();
     926                $start_timestamp  = strtotime( $start );
     927
     928                // Process desktop events
     929                foreach ( $all_events_desktop as $event ) {
     930                    // Events may have 'timestamp', 'time', 't', or 'insert_at' field
     931                    $event_time = null;
     932                    if ( isset( $event['timestamp'] ) ) {
     933                        $event_time = is_numeric( $event['timestamp'] ) ? $event['timestamp'] : strtotime( $event['timestamp'] );
     934                    } elseif ( isset( $event['insert_at'] ) ) {
     935                        $event_time = is_numeric( $event['insert_at'] ) ? $event['insert_at'] : strtotime( $event['insert_at'] );
     936                    } elseif ( isset( $event['time'] ) ) {
     937                        $event_time = is_numeric( $event['time'] ) ? $event['time'] : strtotime( $event['time'] );
     938                    } elseif ( isset( $event['t'] ) ) {
     939                        $event_time = is_numeric( $event['t'] ) ? $event['t'] : strtotime( $event['t'] );
     940                    }
     941
     942                    if ( $event_time && $event_time >= $start_timestamp ) {
     943                        $bucket = gmdate( $group_format === '%Y-%m-%d %H:00:00' ? 'Y-m-d H:00:00' : 'Y-m-d', $event_time );
     944                        if ( ! isset( $events_by_bucket[ $bucket ] ) ) {
     945                            $events_by_bucket[ $bucket ] = array( 'desktop_events' => 0, 'mobile_events' => 0 );
     946                        }
     947                        $events_by_bucket[ $bucket ]['desktop_events']++;
     948                    }
     949                }
     950
     951                // Process mobile events
     952                foreach ( $all_events_mobile as $event ) {
     953                    // Events may have 'timestamp', 'time', 't', or 'insert_at' field
     954                    $event_time = null;
     955                    if ( isset( $event['timestamp'] ) ) {
     956                        $event_time = is_numeric( $event['timestamp'] ) ? $event['timestamp'] : strtotime( $event['timestamp'] );
     957                    } elseif ( isset( $event['insert_at'] ) ) {
     958                        $event_time = is_numeric( $event['insert_at'] ) ? $event['insert_at'] : strtotime( $event['insert_at'] );
     959                    } elseif ( isset( $event['time'] ) ) {
     960                        $event_time = is_numeric( $event['time'] ) ? $event['time'] : strtotime( $event['time'] );
     961                    } elseif ( isset( $event['t'] ) ) {
     962                        $event_time = is_numeric( $event['t'] ) ? $event['t'] : strtotime( $event['t'] );
     963                    }
     964
     965                    if ( $event_time && $event_time >= $start_timestamp ) {
     966                        $bucket = gmdate( $group_format === '%Y-%m-%d %H:00:00' ? 'Y-m-d H:00:00' : 'Y-m-d', $event_time );
     967                        if ( ! isset( $events_by_bucket[ $bucket ] ) ) {
     968                            $events_by_bucket[ $bucket ] = array( 'desktop_events' => 0, 'mobile_events' => 0 );
     969                        }
     970                        $events_by_bucket[ $bucket ]['mobile_events']++;
     971                    }
     972                }
     973
     974                // Convert to array format matching database results
     975                foreach ( $events_by_bucket as $bucket => $counts ) {
     976                    $events_data[] = array(
     977                        'bucket'         => $bucket,
     978                        'desktop_events' => $counts['desktop_events'],
     979                        'mobile_events'  => $counts['mobile_events'],
     980                    );
     981                }
     982
     983                // Sort by bucket
     984                usort( $events_data, function ( $a, $b ) {
     985                    return strcmp( $a['bucket'], $b['bucket'] );
     986                } );
     987            } else {
     988                // Get events from database
     989                if ( $group_format === '%Y-%m-%d %H:00:00' ) {
     990                    $events_data = $wpdb->get_results(
     991                        $wpdb->prepare(
     992                            "SELECT DATE_FORMAT(e.insert_at, '%%Y-%%m-%%d %%H:00:00') AS bucket,
     993                                SUM(CASE WHEN e.event=16 THEN 1 ELSE 0 END) AS desktop_events,
     994                                SUM(CASE WHEN e.event=17 THEN 1 ELSE 0 END) AS mobile_events
     995                             FROM {$wpdb->prefix}optibehavior_events e
     996                             WHERE e.page_id2=%d AND e.insert_at >= %s
     997                             GROUP BY bucket
     998                             ORDER BY bucket ASC",
     999                            $page_id2, $start
     1000                        ),
     1001                        ARRAY_A
     1002                    );
     1003                } else {
     1004                    $events_data = $wpdb->get_results(
     1005                        $wpdb->prepare(
     1006                            "SELECT DATE_FORMAT(e.insert_at, '%%Y-%%m-%%d') AS bucket,
     1007                                SUM(CASE WHEN e.event=16 THEN 1 ELSE 0 END) AS desktop_events,
     1008                                SUM(CASE WHEN e.event=17 THEN 1 ELSE 0 END) AS mobile_events
     1009                             FROM {$wpdb->prefix}optibehavior_events e
     1010                             WHERE e.page_id2=%d AND e.insert_at >= %s
     1011                             GROUP BY bucket
     1012                             ORDER BY bucket ASC",
     1013                            $page_id2, $start
     1014                        ),
     1015                        ARRAY_A
     1016                    );
     1017                }
     1018            }
     1019        }
     1020
     1021        // Merge all data by bucket (time period)
     1022        $merged = array();
     1023
     1024        foreach ( $visitors_data as $row ) {
     1025            $bucket = $row['bucket'];
     1026            if ( ! isset( $merged[ $bucket ] ) ) {
     1027                $merged[ $bucket ] = array(
     1028                    'total_visitors' => 0,
     1029                    'desktop_visitors' => 0,
     1030                    'mobile_visitors' => 0,
     1031                    'desktop_events' => 0,
     1032                    'mobile_events' => 0,
     1033                );
     1034            }
     1035            $merged[ $bucket ]['total_visitors'] = intval( $row['total_sessions'] );
     1036            $merged[ $bucket ]['desktop_visitors'] = intval( $row['desktop_sessions'] );
     1037            $merged[ $bucket ]['mobile_visitors'] = intval( $row['mobile_sessions'] );
     1038        }
     1039
     1040        foreach ( $events_data as $row ) {
     1041            $bucket = $row['bucket'];
     1042            if ( ! isset( $merged[ $bucket ] ) ) {
     1043                $merged[ $bucket ] = array(
     1044                    'total_visitors' => 0,
     1045                    'desktop_visitors' => 0,
     1046                    'mobile_visitors' => 0,
     1047                    'desktop_events' => 0,
     1048                    'mobile_events' => 0,
     1049                );
     1050            }
     1051            $merged[ $bucket ]['desktop_events'] = intval( $row['desktop_events'] );
     1052            $merged[ $bucket ]['mobile_events'] = intval( $row['mobile_events'] );
     1053        }
     1054
     1055        // Sort by bucket and prepare output
     1056        ksort( $merged );
     1057
     1058        $labels = array();
     1059        $total_visitors = array();
     1060        $desktop_visitors = array();
     1061        $mobile_visitors = array();
     1062        $desktop_events = array();
     1063        $mobile_events = array();
     1064
     1065        foreach ( $merged as $bucket => $data ) {
     1066            $labels[] = $bucket;
     1067            $total_visitors[] = $data['total_visitors'];
     1068            $desktop_visitors[] = $data['desktop_visitors'];
     1069            $mobile_visitors[] = $data['mobile_visitors'];
     1070            $desktop_events[] = $data['desktop_events'];
     1071            $mobile_events[] = $data['mobile_events'];
     1072        }
     1073
     1074        wp_send_json_success( array(
     1075            'labels' => $labels,
     1076            'total_visitors' => $total_visitors,
     1077            'desktop_visitors' => $desktop_visitors,
     1078            'mobile_visitors' => $mobile_visitors,
     1079            'desktop_events' => $desktop_events,
     1080            'mobile_events' => $mobile_events,
     1081            'group' => $group_format,
     1082        ) );
     1083    }
     1084
     1085    /**
    5131086     * AJAX handler for post referrers data
    5141087     */
     
    5361109            $url
    5371110        ) ) );
     1111
     1112        // If exact match not found, try with trailing slash variations
     1113        if ( ! $page_id ) {
     1114            $url_alt = rtrim( $url, '/' ) . '/';
     1115            if ( $url_alt === $url ) {
     1116                $url_alt = rtrim( $url, '/' );
     1117            }
     1118            $page_id = intval( $wpdb->get_var( $wpdb->prepare(
     1119                "SELECT id FROM {$wpdb->prefix}optibehavior_pages WHERE url=%s LIMIT 1",
     1120                $url_alt
     1121            ) ) );
     1122        }
    5381123
    5391124        if ( ! $page_id ) {
     
    6011186        ) ) );
    6021187
     1188        // If exact match not found, try with trailing slash variations
     1189        if ( ! $page_id ) {
     1190            $url_alt = rtrim( $url, '/' ) . '/';
     1191            if ( $url_alt === $url ) {
     1192                $url_alt = rtrim( $url, '/' );
     1193            }
     1194            $page_id = intval( $wpdb->get_var( $wpdb->prepare(
     1195                "SELECT id FROM {$wpdb->prefix}optibehavior_pages WHERE url=%s LIMIT 1",
     1196                $url_alt
     1197            ) ) );
     1198        }
     1199
    6031200        if ( ! $page_id ) {
    6041201            // Try matching by normalized url2
     
    6351232        $outbound_clicks = $session->get_page_outbound_clicks( $page_id, 10 );
    6361233
    637         // Ensure Exit Behavior total equals Entry Sources by adding synthetic "Left page" for any deficit
    638         try {
    639             $referrers = $session->get_page_referrers( $page_id, 1000 );
    640             $ref_total = 0;
    641             if ( is_array( $referrers ) ) {
    642                 foreach ( $referrers as $r ) {
    643                     $ref_total += isset( $r['count'] ) ? intval( $r['count'] ) : 0;
    644                 }
    645             }
    646             $exit_total = 0;
    647             if ( is_array( $outbound_clicks ) ) {
    648                 foreach ( $outbound_clicks as $o ) {
    649                     $exit_total += isset( $o['count'] ) ? intval( $o['count'] ) : 0;
    650                 }
    651             }
    652             $deficit = $ref_total - $exit_total;
    653             if ( $deficit > 0 ) {
    654                 $merged = false;
    655                 if ( is_array( $outbound_clicks ) ) {
    656                     foreach ( $outbound_clicks as $idx => $o ) {
    657                         if ( isset( $o['click_type'] ) && $o['click_type'] === 'left' ) {
    658                             $outbound_clicks[ $idx ]['count'] = intval( $outbound_clicks[ $idx ]['count'] ) + $deficit;
    659                             $merged = true;
    660                             break;
    661                         }
    662                     }
    663                 }
    664                 if ( ! $merged ) {
    665                     $outbound_clicks[] = array(
    666                         'target_url'  => 'Left page',
    667                         'click_type'  => 'left',
    668                         'element_tag' => null,
    669                         'element_text'=> null,
    670                         'count'       => $deficit,
    671                         'last_click'  => current_time( 'mysql' ),
    672                     );
    673                 }
    674             }
    675         } catch ( \Throwable $e ) {
    676             // Fail open; show raw outbound clicks if balancing fails
     1234        // Count sessions that truly left without clicking any link
     1235        $total_sessions = intval( $wpdb->get_var( $wpdb->prepare(
     1236            "SELECT COUNT(DISTINCT pv.session_id)
     1237             FROM {$wpdb->prefix}optibehavior_pageviews pv
     1238             WHERE pv.url=%s OR pv.url LIKE %s",
     1239            $url, $url . '%'
     1240        ) ) );
     1241
     1242        $sessions_with_clicks = intval( $wpdb->get_var( $wpdb->prepare(
     1243            "SELECT COUNT(DISTINCT oc.session_id)
     1244             FROM {$wpdb->prefix}optibehavior_outbound_clicks oc
     1245             WHERE oc.page_id=%d",
     1246            $page_id
     1247        ) ) );
     1248
     1249        $sessions_left_without_click = $total_sessions - $sessions_with_clicks;
     1250
     1251        // Only add "Left page" if there are sessions that actually closed the page without clicking
     1252        if ( $sessions_left_without_click > 0 ) {
     1253            // Get the most recent timestamp from sessions that left without clicking
     1254            $last_left = $wpdb->get_var( $wpdb->prepare(
     1255                "SELECT MAX(pv.view_time)
     1256                 FROM {$wpdb->prefix}optibehavior_pageviews pv
     1257                 WHERE (pv.url=%s OR pv.url LIKE %s)
     1258                 AND pv.session_id NOT IN (
     1259                     SELECT DISTINCT session_id
     1260                     FROM {$wpdb->prefix}optibehavior_outbound_clicks
     1261                     WHERE page_id=%d
     1262                 )",
     1263                $url, $url . '%', $page_id
     1264            ) );
     1265
     1266            $outbound_clicks[] = array(
     1267                'target_url'  => 'Left page',
     1268                'click_type'  => 'left',
     1269                'element_tag' => null,
     1270                'element_text'=> null,
     1271                'count'       => $sessions_left_without_click,
     1272                'last_click'  => $last_left ?: current_time( 'mysql' ),
     1273            );
    6771274        }
    6781275
     
    7111308        wp_send_json_success( $outbound_clicks );
    7121309    }
     1310
     1311    /**
     1312     * AJAX handler for post countries data
     1313     */
     1314    public function ajax_post_countries() {
     1315        if ( ! isset( $_POST['post_id'] ) ) {
     1316            wp_send_json_error();
     1317        }
     1318
     1319        $post_id = intval( $_POST['post_id'] );
     1320        check_ajax_referer( 'optibehavior_post_analytics_' . $post_id, 'nonce' );
     1321
     1322        if ( ! current_user_can( 'edit_post', $post_id ) ) {
     1323            wp_send_json_error();
     1324        }
     1325
     1326        global $wpdb;
     1327        $url = get_permalink( $post_id );
     1328        if ( ! $url ) {
     1329            wp_send_json_error();
     1330        }
     1331
     1332        // Get sessions for this URL and their visitor countries
     1333        $results = $wpdb->get_results( $wpdb->prepare(
     1334            "SELECT UPPER(COALESCE(NULLIF(TRIM(vis.country),''), '')) AS country,
     1335                    vis.country_name,
     1336                    COUNT(DISTINCT s.id) AS count
     1337             FROM {$wpdb->prefix}optibehavior_sessions s
     1338             LEFT JOIN {$wpdb->prefix}optibehavior_visitors vis ON s.visitor_id = vis.id
     1339             LEFT JOIN {$wpdb->prefix}optibehavior_pageviews pv ON s.id = pv.session_id
     1340             WHERE (pv.url=%s OR pv.url LIKE %s)
     1341               AND COALESCE(NULLIF(TRIM(vis.country),''),'UN') <> 'UN'
     1342             GROUP BY country, vis.country_name
     1343             ORDER BY count DESC
     1344             LIMIT 10",
     1345            $url, $url . '%'
     1346        ) );
     1347
     1348        $data = array();
     1349        foreach ( $results as $row ) {
     1350            $code = strtoupper( $row->country ?: '' );
     1351            $name = $row->country_name ?: ( $code ?: 'Global' );
     1352            $data[] = array(
     1353                'country' => $code ?: 'Global',
     1354                'country_name' => $name,
     1355                'count' => intval( $row->count ),
     1356            );
     1357        }
     1358
     1359        wp_send_json_success( $data );
     1360    }
     1361
     1362    /**
     1363     * AJAX handler for post browsers data
     1364     */
     1365    public function ajax_post_browsers() {
     1366        if ( ! isset( $_POST['post_id'] ) ) {
     1367            wp_send_json_error();
     1368        }
     1369
     1370        $post_id = intval( $_POST['post_id'] );
     1371        check_ajax_referer( 'optibehavior_post_analytics_' . $post_id, 'nonce' );
     1372
     1373        if ( ! current_user_can( 'edit_post', $post_id ) ) {
     1374            wp_send_json_error();
     1375        }
     1376
     1377        global $wpdb;
     1378        $url = get_permalink( $post_id );
     1379        if ( ! $url ) {
     1380            wp_send_json_error();
     1381        }
     1382
     1383        // Get browsers for sessions viewing this URL
     1384        $results = $wpdb->get_results( $wpdb->prepare(
     1385            "SELECT
     1386                CASE
     1387                    WHEN vis.browser IS NULL OR TRIM(vis.browser) = '' THEN 'Unknown'
     1388                    WHEN LOWER(vis.browser) IN ('unknown','undefined','other') THEN 'Unknown'
     1389                    ELSE TRIM(vis.browser)
     1390                END AS browser,
     1391                COUNT(DISTINCT s.id) AS count
     1392             FROM {$wpdb->prefix}optibehavior_sessions s
     1393             LEFT JOIN {$wpdb->prefix}optibehavior_visitors vis ON s.visitor_id = vis.id
     1394             LEFT JOIN {$wpdb->prefix}optibehavior_pageviews pv ON s.id = pv.session_id
     1395             WHERE (pv.url=%s OR pv.url LIKE %s)
     1396             GROUP BY browser
     1397             ORDER BY count DESC
     1398             LIMIT 10",
     1399            $url, $url . '%'
     1400        ) );
     1401
     1402        $data = array();
     1403        foreach ( $results as $row ) {
     1404            $data[] = array(
     1405                'name' => $row->browser ?: 'Unknown',
     1406                'count' => intval( $row->count ),
     1407            );
     1408        }
     1409
     1410        wp_send_json_success( $data );
     1411    }
     1412
     1413    /**
     1414     * AJAX handler for post devices data
     1415     */
     1416    public function ajax_post_devices() {
     1417        if ( ! isset( $_POST['post_id'] ) ) {
     1418            wp_send_json_error();
     1419        }
     1420
     1421        $post_id = intval( $_POST['post_id'] );
     1422        check_ajax_referer( 'optibehavior_post_analytics_' . $post_id, 'nonce' );
     1423
     1424        if ( ! current_user_can( 'edit_post', $post_id ) ) {
     1425            wp_send_json_error();
     1426        }
     1427
     1428        global $wpdb;
     1429        $url = get_permalink( $post_id );
     1430        if ( ! $url ) {
     1431            wp_send_json_error();
     1432        }
     1433
     1434        // Get device types for sessions viewing this URL
     1435        $results = $wpdb->get_results( $wpdb->prepare(
     1436            "SELECT
     1437                CASE
     1438                    WHEN vis.device_type IS NULL OR TRIM(vis.device_type) = '' THEN 'Unknown'
     1439                    WHEN LOWER(vis.device_type) IN ('unknown','undefined','other') THEN 'Unknown'
     1440                    ELSE TRIM(vis.device_type)
     1441                END AS device,
     1442                COUNT(DISTINCT s.id) AS count
     1443             FROM {$wpdb->prefix}optibehavior_sessions s
     1444             LEFT JOIN {$wpdb->prefix}optibehavior_visitors vis ON s.visitor_id = vis.id
     1445             LEFT JOIN {$wpdb->prefix}optibehavior_pageviews pv ON s.id = pv.session_id
     1446             WHERE (pv.url=%s OR pv.url LIKE %s)
     1447             GROUP BY device
     1448             ORDER BY count DESC
     1449             LIMIT 10",
     1450            $url, $url . '%'
     1451        ) );
     1452
     1453        $data = array();
     1454        foreach ( $results as $row ) {
     1455            $data[] = array(
     1456                'name' => $row->device ?: 'Unknown',
     1457                'count' => intval( $row->count ),
     1458            );
     1459        }
     1460
     1461        wp_send_json_success( $data );
     1462    }
    7131463}
  • opti-behavior/trunk/admin/recordings-upgrade-page.php

    r3399182 r3401441  
    5656                <div class="free-badge">
    5757                    <i data-lucide="gift"></i>
    58                     <span><?php esc_html_e( 'FREE during testing period!', 'opti-behavior' ); ?></span>
     58                    <span><?php esc_html_e( 'FREE During Testing Period!', 'opti-behavior' ); ?></span>
    5959                </div>
    6060               
     
    6363                </p>
    6464
    65                 <a href="<?php echo esc_url( admin_url( 'admin.php?page=opti-behavior-analytics' ) ); ?>" class="button button-primary button-hero">
     65                <a href="https://optiuser.com/" target="_blank" rel="noopener noreferrer" class="button button-primary button-hero">
    6666                    <i data-lucide="download"></i>
    6767                    <?php esc_html_e( 'Download PRO Version', 'opti-behavior' ); ?>
  • opti-behavior/trunk/assets/css/admin-notices.css

    r3399182 r3401441  
    5555
    5656/* Generic message containers */
    57 div[id^="message"],
     57div[id^="message"]:not(#support-form-message),
    5858div[id*="notice"],
    5959div[id*="warning"],
    6060div[id*="error"],
    6161div[class*="notice"],
    62 div[class*="message"],
     62div[class*="message"]:not(.form-message):not(#support-form-message),
    6363div[class*="notification"],
    6464div[class*="alert"],
     
    7272
    7373/* Review/rating notices */
    74 div[class*="review"],
     74div[class*="review"]:not(.ai-feedback-section):not(.feedback-card),
    7575div[class*="rating"],
    76 div[class*="feedback"],
     76div[class*="feedback"]:not(.ai-feedback-section):not(.feedback-card):not(.feedback-icon):not(.feedback-title):not(.feedback-description):not(.feedback-actions):not(.feedback-btn):not(.feedback-note),
    7777
    7878/* Newsletter/subscription notices */
     
    8282/* Promotional notices (third-party). Allow Opti-Behavior upgrade layouts. */
    8383div[class*="promo"]:not([class*="opti-behavior"]),
    84 div[class*="upgrade"]:not([class*="opti-behavior"]):not(.upgrade-card):not(.upgrade-features):not(.upgrade-action):not(.recordings-upgrade-content):not(.upgrade-preview),
     84div[class*="upgrade"]:not([class*="opti-behavior"]):not(.upgrade-card):not(.upgrade-features):not(.upgrade-action):not(.recordings-upgrade-content):not(.upgrade-preview):not(.upgrade-cta):not(.upgrade-description):not(.upgrade-icon):not(.upgrade-title),
    8585div[class*="premium"]:not([class*="opti-behavior"]),
    8686
     
    102102/* Donation/support notices */
    103103div[class*="donate"],
    104 div[class*="support"],
     104div[class*="support"]:not(.support-form):not([id*="support-"]),
    105105
    106106/* Onboarding/welcome notices */
     
    148148}
    149149
     150/* EXCEPTION: Keep AI Insights support form visible */
     151.ai-feedback-section,
     152.ai-feedback-section *,
     153.feedback-card,
     154.support-form,
     155#opti-behavior-support-form {
     156    display: block !important;
     157    visibility: visible !important;
     158    opacity: 1 !important;
     159    height: auto !important;
     160}
     161
  • opti-behavior/trunk/assets/css/ai-insights-page.css

    r3399182 r3401441  
    414414    }
    415415}
     416
     417/* Support Form Styles */
     418.support-form {
     419    max-width: 600px;
     420    margin: 32px auto;
     421    text-align: left;
     422}
     423
     424.support-form .form-group {
     425    margin-bottom: 24px;
     426}
     427
     428.support-form label {
     429    display: block;
     430    font-size: 15px;
     431    font-weight: 600;
     432    color: white;
     433    margin-bottom: 8px;
     434}
     435
     436.support-form .form-control {
     437    width: 100%;
     438    padding: 12px 16px;
     439    font-size: 15px;
     440    border: 2px solid rgba(255, 255, 255, 0.3);
     441    border-radius: 10px;
     442    background: rgba(255, 255, 255, 0.95);
     443    color: #1e293b;
     444    transition: all 0.3s ease;
     445    box-sizing: border-box;
     446}
     447
     448.support-form .form-control:focus {
     449    outline: none;
     450    border-color: white;
     451    background: white;
     452    box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.2);
     453}
     454
     455.support-form textarea.form-control {
     456    resize: vertical;
     457    min-height: 120px;
     458    font-family: inherit;
     459}
     460
     461.support-form .form-hint {
     462    display: block;
     463    font-size: 13px;
     464    color: rgba(255, 255, 255, 0.8);
     465    margin-top: 6px;
     466}
     467
     468.support-form .form-actions {
     469    text-align: center;
     470    margin-top: 32px;
     471}
     472
     473.support-form button[type="submit"] {
     474    border: none;
     475    cursor: pointer;
     476}
     477
     478.support-form button[type="submit"]:disabled {
     479    opacity: 0.7;
     480    cursor: not-allowed;
     481}
     482
     483.support-form .support-response {
     484    margin-top: 24px;
     485    padding: 16px 20px;
     486    border-radius: 10px;
     487    font-size: 15px;
     488    font-weight: 600;
     489    text-align: center;
     490}
     491
     492.support-form .support-response-hidden {
     493    display: none !important;
     494}
     495
     496.support-form .support-response.success {
     497    background: rgba(16, 185, 129, 0.2);
     498    color: white;
     499    border: 2px solid rgba(255, 255, 255, 0.5);
     500}
     501
     502.support-form .support-response.error {
     503    background: rgba(239, 68, 68, 0.2);
     504    color: white;
     505    border: 2px solid rgba(239, 68, 68, 0.5);
     506}
     507
     508/* Responsive adjustments for support form */
     509@media (max-width: 768px) {
     510    .support-form {
     511        max-width: 100%;
     512    }
     513}
  • opti-behavior/trunk/assets/css/heatmaps.css

    r3399182 r3401441  
    6565
    6666.btn-filter.active {
    67     background: #6c5ce7;
    68     border-color: #6c5ce7;
     67    background: #16a34a;
     68    border-color: #16a34a;
    6969    color: white;
    7070}
     
    145145
    146146.stat-change.positive {
    147     color: #28a745;
     147    color: #16a34a;
    148148}
    149149
     
    209209
    210210.btn-action.primary {
    211     background: #6c5ce7;
    212     border-color: #6c5ce7;
     211    background: #16a34a;
     212    border-color: #16a34a;
    213213    color: white;
    214214}
    215215
    216216.btn-action.primary:hover {
    217     background: #5a4fcf;
     217    background: #15803d;
    218218}
    219219
     
    263263
    264264.heatmap-table th {
    265     background: #f8f9fa;
    266     border-bottom: 2px solid #e1e5e9;
     265    background: linear-gradient(135deg, #dcfce7 0%, #f0fdf4 100%);
     266    border-bottom: 2px solid #86efac;
    267267    padding: 16px;
    268268    text-align: center;
    269269    font-size: 14px;
    270270    font-weight: 600;
    271     color: #495057;
     271    color: #166534;
    272272}
    273273
     
    333333
    334334.heatmaps-modern-table thead th {
    335     background: transparent !important;
    336     border-bottom: 2px solid #e2e8f0 !important;
     335    background: linear-gradient(135deg, #dcfce7 0%, #f0fdf4 100%) !important;
     336    border-bottom: 2px solid #86efac !important;
    337337    padding: 16px 20px !important;
    338338    text-align: left !important;
    339339    font-size: 13px !important;
    340340    font-weight: 600 !important;
    341     color: #475569 !important;
     341    color: #166534 !important;
    342342    text-transform: uppercase !important;
    343343    letter-spacing: 0.05em !important;
     
    350350
    351351.heatmaps-modern-table tbody tr.heatmap-table-row:hover {
    352     background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%) !important;
    353     box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
     352    background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%) !important;
     353    box-shadow: 0 2px 8px rgba(22, 163, 74, 0.15);
    354354    transform: translateY(-1px);
    355355}
     
    416416
    417417.heatmaps-modern-table .heatmap-action-btn:hover {
    418     background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
    419     border-color: #2563eb !important;
     418    background: linear-gradient(135deg, #16a34a 0%, #22c55e 100%) !important;
     419    border-color: #16a34a !important;
    420420    color: white !important;
    421421    transform: translateY(-1px) !important;
    422     box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3) !important;
     422    box-shadow: 0 4px 12px rgba(22, 163, 74, 0.3) !important;
    423423}
    424424
     
    477477    align-items: center;
    478478    gap: 6px;
    479     background: #6c5ce7;
     479    background: #16a34a;
    480480    color: white;
    481481    text-decoration: none;
     
    488488
    489489.btn-view:hover {
    490     background: #5a4fcf;
     490    background: #15803d;
    491491    color: white;
    492492    text-decoration: none;
     
    497497    align-items: center;
    498498    gap: 6px;
    499     background: #00b894;
     499    background: #16a34a;
    500500    color: white;
    501501    text-decoration: none;
     
    509509
    510510.btn-view-pc:hover {
    511     background: #00a085;
     511    background: #15803d;
    512512    color: white;
    513513    text-decoration: none;
     
    518518    align-items: center;
    519519    gap: 6px;
    520     background: #fd79a8;
     520    background: #16a34a;
    521521    color: white;
    522522    text-decoration: none;
     
    530530
    531531.btn-view-mobile:hover {
    532     background: #e84393;
     532    background: #15803d;
    533533    color: white;
    534534    text-decoration: none;
     
    552552}
    553553
     554/* Green theme for Desktop and Mobile action buttons */
     555.optibehavior-btn-desktop.button,
     556.optibehavior-btn-mobile.button {
     557    background: #16a34a !important;
     558    border-color: #16a34a !important;
     559    color: white !important;
     560    text-shadow: none !important;
     561    box-shadow: none !important;
     562}
     563
     564.optibehavior-btn-desktop.button:hover,
     565.optibehavior-btn-mobile.button:hover {
     566    background: #15803d !important;
     567    border-color: #15803d !important;
     568    color: white !important;
     569}
     570
     571/* Ensure consistent icon sizes in Desktop/Mobile buttons */
     572.optibehavior-btn-desktop.button svg,
     573.optibehavior-btn-mobile.button svg,
     574.optibehavior-btn-desktop.button i[data-lucide],
     575.optibehavior-btn-mobile.button i[data-lucide] {
     576    width: 14px !important;
     577    height: 14px !important;
     578    min-width: 14px !important;
     579    min-height: 14px !important;
     580    max-width: 14px !important;
     581    max-height: 14px !important;
     582    vertical-align: middle !important;
     583    margin-right: 4px !important;
     584    display: inline-block !important;
     585}
     586
    554587.no-heatmaps {
    555588    text-align: center;
     
    588621
    589622.btn-apply {
    590     background: #6c5ce7;
     623    background: #16a34a;
    591624    color: white;
    592625    border: none;
     
    612645
    613646.view-btn.active {
    614     background: #6c5ce7;
    615     border-color: #6c5ce7;
     647    background: #16a34a;
     648    border-color: #16a34a;
    616649    color: white;
    617650}
     
    687720
    688721.btn-preview {
    689     background: #6c5ce7;
     722    background: #16a34a;
    690723    color: white;
    691724    border: none;
     
    822855.heatmaps-panel .table-pagination .pagination-btn[data-next],
    823856.heatmaps-panel .table-pagination .pagination-btn[data-last] {
    824     background:#4f46e5; border-color:#4f46e5; color:#fff;
     857    background:#16a34a; border-color:#16a34a; color:#fff;
    825858}
    826859.heatmaps-panel .table-pagination .pagination-btn[data-next]:hover,
    827 .heatmaps-panel .table-pagination .pagination-btn[data-last]:hover { background:#4338ca; }
     860.heatmaps-panel .table-pagination .pagination-btn[data-last]:hover { background:#15803d; }
    828861
    829862/* Current page badge */
    830863.heatmaps-panel .table-pagination .pagination-current {
    831     background:#eef2ff; color:#4f46e5; border-radius:8px; padding:6px 10px; font-size:12px; font-weight:600;
     864    background:#dcfce7; color:#16a34a; border-radius:8px; padding:6px 10px; font-size:12px; font-weight:600;
    832865}
    833866.heatmaps-panel .table-pagination .pagination-ellipsis { color:#94a3b8; }
     
    902935
    903936.stat-badge {
    904     background: #6c5ce7;
     937    background: #16a34a;
    905938    color: white;
    906939    border-radius: 12px;
  • opti-behavior/trunk/assets/css/metabox.css

    r3399182 r3401441  
    119119}
    120120
     121.optibehavior-behavior-label {
     122    font-size: 12px;
     123    color: #64748b;
     124    margin-bottom: 8px;
     125    font-weight: 500;
     126    display: flex;
     127    align-items: center;
     128}
     129
     130.optibehavior-behavior-value {
     131    font-size: 18px;
     132    font-weight: 700;
     133    color: #1e293b;
     134}
     135
     136.optibehavior-stat-badge {
     137    display: inline-flex;
     138    align-items: center;
     139    justify-content: center;
     140    background: #10b981;
     141    color: #fff;
     142    padding: 4px 8px;
     143    border-radius: 12px;
     144    font-size: 11px;
     145    font-weight: 600;
     146    min-width: 32px;
     147    text-align: center;
     148}
     149
    121150@media (max-width: 480px) {
    122151    .optibehavior-analytics-grid {
     
    193222    background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
    194223    color: #fff;
     224}
     225
     226/* Secondary grid for new analytics row */
     227.optibehavior-secondary-grid {
     228    margin-top: 16px;
     229    padding-top: 16px;
     230    border-top: 1px solid #e5e7eb;
    195231}
    196232
     
    244280    padding-right: 12px;
    245281}
     282
     283/* Loading and empty states */
     284.optibehavior-loading {
     285    display: block;
     286    padding: 20px;
     287    text-align: center;
     288    color: #64748b;
     289    font-size: 12px;
     290}
     291
     292.optibehavior-empty {
     293    display: none;
     294    padding: 20px;
     295    text-align: center;
     296    color: #94a3b8;
     297    font-size: 12px;
     298    font-style: italic;
     299}
     300
     301/* Empty state with icon (matching dashboard style) */
     302.optibehavior-empty-state {
     303    display: none;
     304    flex-direction: column;
     305    align-items: center;
     306    justify-content: center;
     307    padding: 40px 20px;
     308    text-align: center;
     309    min-height: 200px;
     310}
     311
     312.optibehavior-empty-state.is-visible {
     313    display: flex;
     314}
     315
     316.optibehavior-empty-title {
     317    margin-top: 12px;
     318    font-size: 14px;
     319    font-weight: 600;
     320    color: #1e293b;
     321}
     322
     323.optibehavior-empty-sub {
     324    margin-top: 4px;
     325    font-size: 12px;
     326    color: #64748b;
     327}
  • opti-behavior/trunk/assets/css/recordings-upgrade.css

    r3399182 r3401441  
    33    margin: 0;
    44    padding: 0;
     5    background: #f8f9fa;
    56}
    67
     
    89    background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
    910    color: #ffffff;
    10     padding: 36px 40px;
    11     margin: 0 0 30px -20px;
    12     box-shadow: 0 4px 12px rgba(99, 102, 241, 0.15);
    13     border-bottom: 3px solid rgba(255, 255, 255, 0.2);
     11    padding: 48px 40px;
     12    margin: 0 0 40px -20px;
     13    box-shadow: 0 8px 24px rgba(99, 102, 241, 0.25);
     14    border-bottom: 4px solid rgba(255, 255, 255, 0.15);
     15    position: relative;
     16    overflow: hidden;
     17}
     18
     19.recordings-header::before {
     20    content: '';
     21    position: absolute;
     22    top: 0;
     23    left: 0;
     24    right: 0;
     25    bottom: 0;
     26    background: url('data:image/svg+xml,<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="50" r="2" fill="white" opacity="0.1"/></svg>');
     27    background-size: 50px 50px;
     28    opacity: 0.3;
    1429}
    1530
     
    1732    max-width: 1400px;
    1833    margin: 0 auto;
     34    position: relative;
     35    z-index: 1;
    1936}
    2037
     
    2239    display: flex;
    2340    align-items: center;
    24     gap: 12px;
    25     font-size: 28px;
    26     font-weight: 600;
    27     margin: 0 0 8px;
     41    gap: 14px;
     42    font-size: 32px;
     43    font-weight: 700;
     44    margin: 0 0 12px;
     45    color: #ffffff !important;
     46    text-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    2847}
    2948
    3049.recordings-title i {
    31     width: 28px;
    32     height: 28px;
     50    width: 32px;
     51    height: 32px;
     52    filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
    3353}
    3454
    3555.recordings-subtitle {
    36     font-size: 14px;
    37     opacity: 0.9;
     56    font-size: 16px;
     57    opacity: 0.95;
     58    font-weight: 400;
     59    letter-spacing: 0.01em;
    3860}
    3961
    4062.recordings-upgrade-content {
    41     max-width: 960px;
     63    max-width: 1000px;
    4264    margin: 0 auto 40px;
    4365    padding: 0 20px 40px;
     
    4567    flex-direction: column;
    4668    align-items: center;
    47     gap: 24px;
     69    gap: 32px;
    4870}
    4971
     
    5173.upgrade-preview {
    5274    background: #ffffff;
    53     border-radius: 12px;
     75    border-radius: 16px;
    5476    border: 1px solid #e5e7eb;
    55     box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04);
    56     padding: 32px 30px;
     77    box-shadow: 0 4px 16px rgba(15, 23, 42, 0.08);
     78    padding: 48px 40px;
    5779    width: 100%;
     80    transition: transform 0.3s ease, box-shadow 0.3s ease;
     81}
     82
     83.upgrade-card:hover {
     84    transform: translateY(-2px);
     85    box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);
    5886}
    5987
    6088.upgrade-icon {
    61     margin-bottom: 20px;
     89    margin-bottom: 24px;
     90    display: flex;
     91    justify-content: center;
    6292}
    6393
    6494.upgrade-icon i {
    65     width: 64px;
    66     height: 64px;
     95    width: 80px;
     96    height: 80px;
    6797    color: #6366f1;
     98    animation: pulse-icon 2s ease-in-out infinite;
     99}
     100
     101@keyframes pulse-icon {
     102    0%, 100% {
     103        transform: scale(1);
     104        opacity: 1;
     105    }
     106    50% {
     107        transform: scale(1.05);
     108        opacity: 0.9;
     109    }
    68110}
    69111
    70112.upgrade-title {
    71     font-size: 26px;
    72     font-weight: 600;
    73     color: #111827;
    74     margin: 0 0 12px;
    75 }
    76 
    77 .upgrade-description {
    78     font-size: 15px;
    79     color: #4b5563;
    80     max-width: 620px;
    81     margin: 0 auto 28px;
    82     line-height: 1.6;
    83 }
    84 
    85 .upgrade-features {
    86     background: #f9fafb;
    87     border-radius: 10px;
    88     padding: 24px 22px;
    89     margin: 0 0 30px;
    90     text-align: left;
    91 }
    92 
    93 .upgrade-features h3 {
    94     font-size: 18px;
    95     font-weight: 600;
     113    font-size: 32px;
     114    font-weight: 700;
    96115    color: #111827;
    97116    margin: 0 0 16px;
     117    text-align: center;
     118}
     119
     120.upgrade-description {
     121    font-size: 16px;
     122    color: #4b5563;
     123    max-width: 680px;
     124    margin: 0 auto 32px;
     125    line-height: 1.7;
     126    text-align: center;
     127}
     128
     129.upgrade-features {
     130    background: linear-gradient(135deg, #f9fafb 0%, #ffffff 100%);
     131    border-radius: 12px;
     132    padding: 32px 28px;
     133    margin: 0 0 36px;
     134    text-align: left;
     135    border: 1px solid #e5e7eb;
     136}
     137
     138.upgrade-features h3 {
     139    font-size: 22px;
     140    font-weight: 700;
     141    color: #111827;
     142    margin: 0 0 24px;
    98143    text-align: center;
    99144}
     
    103148    margin: 0;
    104149    padding: 0;
     150    display: grid;
     151    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
     152    gap: 16px;
    105153}
    106154
     
    108156    display: flex;
    109157    align-items: center;
    110     gap: 12px;
    111     padding: 10px 0;
    112     font-size: 14px;
     158    gap: 14px;
     159    padding: 14px 16px;
     160    font-size: 15px;
    113161    color: #1f2933;
    114     border-bottom: 1px solid #e5e7eb;
    115 }
    116 
    117 .features-list li:last-child {
    118     border-bottom: none;
     162    background: #ffffff;
     163    border-radius: 8px;
     164    border: 1px solid #e5e7eb;
     165    transition: all 0.2s ease;
     166}
     167
     168.features-list li:hover {
     169    border-color: #10b981;
     170    background: #f0fdf4;
     171    transform: translateX(4px);
    119172}
    120173
    121174.features-list li i {
    122175    color: #10b981;
     176    width: 24px;
     177    height: 24px;
     178    flex-shrink: 0;
     179}
     180
     181.upgrade-cta {
     182    margin-top: 40px;
     183    text-align: center;
     184    padding: 32px;
     185    background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
     186    border-radius: 12px;
     187    border: 2px solid #bae6fd;
     188}
     189
     190.free-badge {
     191    display: inline-flex;
     192    align-items: center;
     193    gap: 10px;
     194    background: linear-gradient(135deg, #10b981 0%, #059669 100%);
     195    color: #ffffff;
     196    padding: 12px 24px;
     197    border-radius: 999px;
     198    font-size: 15px;
     199    font-weight: 700;
     200    box-shadow: 0 6px 16px rgba(16, 185, 129, 0.5);
     201    margin-bottom: 20px;
     202    animation: badge-pulse 2s ease-in-out infinite;
     203}
     204
     205@keyframes badge-pulse {
     206    0%, 100% {
     207        box-shadow: 0 6px 16px rgba(16, 185, 129, 0.5);
     208    }
     209    50% {
     210        box-shadow: 0 8px 20px rgba(16, 185, 129, 0.7);
     211    }
     212}
     213
     214.free-badge i {
    123215    width: 20px;
    124216    height: 20px;
    125     flex-shrink: 0;
    126 }
    127 
    128 .upgrade-cta {
    129     margin-top: 32px;
    130 }
    131 
    132 .free-badge {
    133     display: inline-flex;
    134     align-items: center;
    135     gap: 8px;
    136     background: linear-gradient(135deg, #10b981 0%, #059669 100%);
    137     color: #ffffff;
    138     padding: 10px 20px;
    139     border-radius: 999px;
    140     font-size: 14px;
    141     font-weight: 600;
    142     box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
    143     margin-bottom: 18px;
    144 }
    145 
    146 .free-badge i {
    147     width: 18px;
    148     height: 18px;
    149217}
    150218
    151219.cta-text {
    152     font-size: 14px;
    153     color: #4b5563;
    154     max-width: 600px;
    155     margin: 0 auto 24px;
    156     line-height: 1.6;
     220    font-size: 15px;
     221    color: #1f2937;
     222    max-width: 640px;
     223    margin: 0 auto 28px;
     224    line-height: 1.7;
     225    font-weight: 500;
    157226}
    158227
     
    160229    display: inline-flex;
    161230    align-items: center;
    162     gap: 10px;
    163     font-size: 15px;
    164     padding: 11px 28px;
     231    gap: 12px;
     232    font-size: 16px;
     233    padding: 14px 32px;
    165234    height: auto;
    166235    line-height: 1.5;
     236    font-weight: 600;
     237    border-radius: 8px;
     238    transition: all 0.3s ease;
     239    background: #2271b1;
     240    border-color: #2271b1;
     241}
     242
     243.button-hero:hover {
     244    background: #135e96;
     245    border-color: #135e96;
     246    transform: translateY(-2px);
     247    box-shadow: 0 8px 16px rgba(34, 113, 177, 0.3);
    167248}
    168249
    169250.button-hero i {
    170     width: 20px;
    171     height: 20px;
     251    width: 22px;
     252    height: 22px;
    172253}
    173254
    174255.help-text {
    175     margin-top: 18px;
    176     font-size: 13px;
     256    margin-top: 20px;
     257    font-size: 14px;
    177258    color: #6b7280;
     259    font-style: italic;
     260}
     261
     262.upgrade-preview {
     263    display: none;
    178264}
    179265
    180266.upgrade-preview h3 {
    181     font-size: 20px;
    182     font-weight: 600;
     267    font-size: 24px;
     268    font-weight: 700;
    183269    color: #111827;
    184     margin: 0 0 18px;
     270    margin: 0 0 24px;
     271    text-align: center;
    185272}
    186273
    187274.preview-placeholder {
    188     background: #f9fafb;
    189     border-radius: 10px;
    190     border: 2px dashed #e5e7eb;
    191     padding: 48px 20px;
     275    background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%);
     276    border-radius: 12px;
     277    border: 2px dashed #d1d5db;
     278    padding: 64px 20px;
    192279    display: flex;
    193280    flex-direction: column;
    194281    align-items: center;
    195     gap: 16px;
     282    gap: 20px;
     283    position: relative;
     284    overflow: hidden;
     285}
     286
     287.preview-placeholder::before {
     288    content: '';
     289    position: absolute;
     290    top: 0;
     291    left: 0;
     292    right: 0;
     293    bottom: 0;
     294    background: radial-gradient(circle at 50% 50%, rgba(99, 102, 241, 0.05) 0%, transparent 70%);
    196295}
    197296
    198297.preview-placeholder i {
    199     width: 96px;
    200     height: 96px;
    201     color: #cbd5f5;
     298    width: 120px;
     299    height: 120px;
     300    color: #a5b4fc;
     301    position: relative;
     302    z-index: 1;
    202303}
    203304
    204305.preview-placeholder p {
    205306    margin: 0;
    206     font-size: 14px;
     307    font-size: 15px;
    207308    color: #6b7280;
     309    position: relative;
     310    z-index: 1;
     311    font-weight: 500;
    208312}
    209313
     
    211315    .recordings-header {
    212316        margin-left: 0;
    213         padding: 28px 20px;
     317        padding: 36px 24px;
    214318    }
    215319
    216320    .recordings-upgrade-content {
     321        padding: 0 20px 30px;
     322    }
     323
     324    .features-list {
    217325        grid-template-columns: 1fr;
    218         padding: 0 20px 30px;
    219326    }
    220327}
    221328
    222329@media (max-width: 600px) {
     330    .recordings-header {
     331        padding: 28px 16px;
     332    }
     333
     334    .recordings-title {
     335        font-size: 24px;
     336    }
     337
     338    .recordings-subtitle {
     339        font-size: 14px;
     340    }
     341
    223342    .upgrade-card,
    224343    .upgrade-preview {
     344        padding: 32px 24px;
     345    }
     346
     347    .upgrade-title {
     348        font-size: 26px;
     349    }
     350
     351    .upgrade-description {
     352        font-size: 15px;
     353    }
     354
     355    .upgrade-features h3 {
     356        font-size: 20px;
     357    }
     358
     359    .features-list li {
     360        font-size: 14px;
     361        padding: 12px 14px;
     362    }
     363
     364    .upgrade-cta {
    225365        padding: 24px 20px;
    226366    }
    227 }
    228 
     367
     368    .button-hero {
     369        width: 100%;
     370        justify-content: center;
     371    }
     372}
     373
  • opti-behavior/trunk/assets/css/settings.css

    r3399182 r3401441  
    14191419}
    14201420
     1421/* Quota Analytics Section */
     1422.quota-analytics {
     1423    margin-top: 20px;
     1424    padding: 20px;
     1425    background: #f8f9fa;
     1426    border-radius: 8px;
     1427    border: 1px solid #e9ecef;
     1428}
     1429
     1430.analytics-title {
     1431    font-size: 16px;
     1432    font-weight: 600;
     1433    color: #2c3e50;
     1434    margin: 0 0 15px 0;
     1435}
     1436
     1437.analytics-grid {
     1438    display: grid;
     1439    grid-template-columns: repeat(3, 1fr);
     1440    gap: 15px;
     1441    margin-bottom: 15px;
     1442}
     1443
     1444.analytics-item {
     1445    display: flex;
     1446    align-items: center;
     1447    gap: 12px;
     1448    background: #ffffff;
     1449    padding: 15px;
     1450    border-radius: 6px;
     1451    border: 1px solid #e9ecef;
     1452}
     1453
     1454.analytics-icon {
     1455    font-size: 24px;
     1456    flex-shrink: 0;
     1457}
     1458
     1459.analytics-data {
     1460    flex: 1;
     1461}
     1462
     1463.analytics-value {
     1464    font-size: 24px;
     1465    font-weight: 700;
     1466    color: #6c5ce7;
     1467    line-height: 1;
     1468}
     1469
     1470.analytics-label {
     1471    font-size: 12px;
     1472    color: #6c757d;
     1473    margin-top: 4px;
     1474}
     1475
     1476.analytics-note {
     1477    margin: 0;
     1478    padding: 10px;
     1479    background: #fff3cd;
     1480    border-radius: 4px;
     1481    border: 1px solid #ffeaa7;
     1482}
     1483
     1484.analytics-note small {
     1485    color: #856404;
     1486    font-size: 13px;
     1487}
     1488
    14211489/* Responsive adjustments for quota stats */
    14221490@media (max-width: 768px) {
     
    14291497        font-size: 24px;
    14301498    }
    1431 }
     1499
     1500    .analytics-grid {
     1501        grid-template-columns: 1fr;
     1502    }
     1503
     1504    .analytics-value {
     1505        font-size: 20px;
     1506    }
     1507}
  • opti-behavior/trunk/assets/js/dashboard.js

    r3399182 r3401441  
    260260            }
    261261
    262             // We have data - show the table
     262            // We have data - show the table and hide empty state
    263263            const table = document.querySelector('.device-types-legend');
    264264            if (table) {
    265265              table.style.display = '';
     266            }
     267
     268            // Hide empty state if it exists
     269            const container = ctx.parentElement;
     270            if (container) {
     271              const emptyState = container.querySelector('.optibehavior-empty-state');
     272              if (emptyState) {
     273                emptyState.style.display = 'none';
     274              }
    266275            }
    267276
     
    359368              'ChromeOS': '#4285f4'
    360369            };
     370
     371            // Hide empty state if it exists (data is available)
     372            const container = ctx.parentElement;
     373            if (container) {
     374              const emptyState = container.querySelector('.optibehavior-empty-state');
     375              if (emptyState) {
     376                emptyState.style.display = 'none';
     377              }
     378            }
    361379
    362380            // Update legend table
     
    14981516
    14991517                if (labels.length > 0) {
     1518                  // Hide empty state if it exists (data is available)
     1519                  const container = intentEl.parentElement;
     1520                  if (container) {
     1521                    const emptyState = container.querySelector('.optibehavior-empty-state');
     1522                    if (emptyState) {
     1523                      emptyState.style.display = 'none';
     1524                    }
     1525                  }
     1526
    15001527                  // Destroy existing chart if it exists
    15011528                  const existingIntentChart = Chart.getChart(intentEl);
     
    30343061
    30353062                var rows = resp.data.items.slice(0,10).map(function(it,idx){
     3063                    // Display username with profile link if available, otherwise show visitor ID
     3064                    var visitorDisplay = '';
     3065                    if (it.user_id && it.user_id > 0 && it.display_name) {
     3066                        // User is a logged-in WordPress user - show display name with profile link
     3067                        visitorDisplay = '<a href="' + esc(it.profile_url||'') + '" target="_blank" rel="noopener" title="View user profile">' +
     3068                            esc(it.display_name) + '</a>';
     3069                    } else {
     3070                        // Anonymous visitor - show visitor ID
     3071                        visitorDisplay = esc(it.visitor_id||'');
     3072                    }
     3073
    30363074                    return '<tr>'+
    30373075                        '<td class="tu-rank">'+ (idx+1) +'</td>'+
    3038                         '<td class="tu-visitor">'+ esc(it.visitor_id||'') +'</td>'+
     3076                        '<td class="tu-visitor">'+ visitorDisplay +'</td>'+
    30393077                        '<td class="tu-num">'+ (it.daily_freq||0).toFixed(2) +'</td>'+
    30403078                        '<td class="tu-num"><span class="tu-chip">'+ fmt(it.avg_session_time||0) +'</span></td>'+
     
    33403378            }
    33413379
    3342             // We have data - show the table container
     3380            // We have data - show the table container and hide empty state
    33433381            var tableContainer = document.querySelector('.resolution-table-container');
    33443382            if (tableContainer) {
    33453383                tableContainer.style.display = '';
     3384            }
     3385
     3386            // Hide empty state if it exists (data is available)
     3387            var container = ctx.parentElement;
     3388            if (container) {
     3389                var emptyState = container.querySelector('.optibehavior-empty-state');
     3390                if (emptyState) {
     3391                    emptyState.style.display = 'none';
     3392                }
    33463393            }
    33473394
  • opti-behavior/trunk/assets/js/heatmaps.js

    r3399182 r3401441  
    2525        if(wrap){ wrap.dataset.orderby = orderby; wrap.dataset.order = order; }
    2626        var oldShell = document.querySelector('.simplified-heatmap-table'); if(oldShell){ oldShell.style.display='none'; }
    27         if(window.optibehaviorUpdateSessionsCounts){ try{ window.optibehaviorUpdateSessionsCounts(container); }catch(e){ /* console.warn('[optibehavior] sessions update error', e); */ } }
     27        // Sessions are now included in main query, no need for lazy update
     28        // if(window.optibehaviorUpdateSessionsCounts){ try{ window.optibehaviorUpdateSessionsCounts(container); }catch(e){ /* console.warn('[optibehavior] sessions update error', e); */ } }
    2829        // Initialize Lucide icons after rendering
    2930        if(typeof lucide !== 'undefined'){ lucide.createIcons(); }
  • opti-behavior/trunk/assets/js/metabox-analytics.js

    r3399182 r3401441  
    66    'use strict';
    77
    8     document.addEventListener('DOMContentLoaded', function() {
     8    // Initialize immediately when script loads (don't wait for DOMContentLoaded)
     9    function initMetaboxAnalytics() {
    910        var box = document.querySelector('.optibehavior-analytics-container');
    1011        if (!box) return;
     
    1516        var nonce = data.nonce || box.getAttribute('data-nonce');
    1617        var ajax = data.ajaxUrl || (typeof window.ajaxurl === 'string' ? window.ajaxurl : null);
    17        
     18
    1819        if (!ajax || !pid || !nonce) return;
    1920
     
    6162        var now = new Date();
    6263        var ageInHours = (now - postDate) / (1000 * 60 * 60);
    63         var defaultRange = ageInHours < 24 ? '24h' : '7d';
    64 
    65         // Load initial analytics data
    66         fetch(ajax, {
    67             method: 'POST',
    68             headers: {'Content-Type': 'application/x-www-form-urlencoded'},
    69             body: 'action=optibehavior_post_analytics&post_id=' + encodeURIComponent(pid) + '&nonce=' + encodeURIComponent(nonce)
    70         })
    71         .then(function(r) { return r.json(); })
    72         .then(function(resp) {
     64        var defaultRange = '24h'; // Default to Last 24 Hours
     65
     66        // Load initial analytics data with minimal delay for async performance
     67        setTimeout(function() {
     68            fetch(ajax, {
     69                method: 'POST',
     70                headers: {'Content-Type': 'application/x-www-form-urlencoded'},
     71                body: 'action=optibehavior_post_analytics&post_id=' + encodeURIComponent(pid) + '&nonce=' + encodeURIComponent(nonce)
     72            })
     73            .then(function(r) { return r.json(); })
     74            .then(function(resp) {
    7375            if (!resp || !resp.success || !resp.data) {
     76                // Hide loading, show empty message
     77                if (qs('#optibehavior-chart-loading')) qs('#optibehavior-chart-loading').style.display = 'none';
    7478                qs('#optibehavior-empty').style.display = 'block';
    7579                qs('#optibehavior-chart').style.display = 'none';
    7680                return;
    7781            }
    78             // Hide empty message and show chart when data is available
     82            // Hide loading and empty message, show chart when data is available
     83            if (qs('#optibehavior-chart-loading')) qs('#optibehavior-chart-loading').style.display = 'none';
    7984            qs('#optibehavior-empty').style.display = 'none';
    8085            qs('#optibehavior-chart').style.display = 'block';
     
    8287            qs('#optibehavior-int').textContent = fmt(resp.data.total_interactions || 0);
    8388            qs('#optibehavior-avg').textContent = fmtDur(resp.data.avg_time);
     89            qs('#optibehavior-scroll').textContent = (resp.data.avg_scroll_depth || 0) + '%';
     90            qs('#optibehavior-bounce').textContent = (resp.data.bounce_rate || 0) + '%';
    8491            // Device split with Lucide icons
    8592            var splitEl = qs('#optibehavior-split');
    86             splitEl.innerHTML = (resp.data.mobile_pct || 0) + '% <i data-lucide="smartphone" style="width:14px;height:14px;vertical-align:middle;"></i> / ' + (100 - (resp.data.mobile_pct || 0)) + '% <i data-lucide="monitor" style="width:14px;height:14px;vertical-align:middle;"></i>';
     93            splitEl.innerHTML = (100 - (resp.data.mobile_pct || 0)) + '% <i data-lucide="monitor" style="width:14px;height:14px;vertical-align:middle;"></i> / ' + (resp.data.mobile_pct || 0) + '% <i data-lucide="smartphone" style="width:14px;height:14px;vertical-align:middle;"></i>';
    8794            // Re-initialize Lucide icons for the new elements
    8895            if (typeof lucide !== 'undefined') {
     
    114121        })
    115122        .catch(function() {
     123            // Hide loading, show empty message on error
     124            if (qs('#optibehavior-chart-loading')) qs('#optibehavior-chart-loading').style.display = 'none';
    116125            qs('#optibehavior-empty').style.display = 'block';
     126            qs('#optibehavior-chart').style.display = 'none';
    117127        });
     128        }, 50); // 50ms delay to allow page to render first
    118129
    119130        function buildChart(range) {
    120             var body = 'action=optibehavior_post_timeseries&post_id=' + encodeURIComponent(pid) + '&nonce=' + encodeURIComponent(nonce) + '&range=' + encodeURIComponent(range);
     131            var body = 'action=optibehavior_post_analytics_chart&post_id=' + encodeURIComponent(pid) + '&nonce=' + encodeURIComponent(nonce) + '&range=' + encodeURIComponent(range);
    121132            fetch(ajax, {
    122133                method: 'POST',
     
    134145                var width = (container && container.clientWidth) ? container.clientWidth : (canvas.clientWidth || 600);
    135146                canvas.width = Math.max(300, width);
    136                 canvas.height = 220; // fixed height for stability
     147                canvas.height = 280; // increased height for better visibility
    137148                var ctx = canvas.getContext('2d');
    138149                if (window.optibehaviorChart) {
     
    148159                        labels: data.labels,
    149160                        datasets: [
    150                             {label: desktopLabel, data: data.pc, borderColor: '#2563eb', backgroundColor: 'rgba(37,99,235,.2)', tension: 0.25, fill: true},
    151                             {label: mobileLabel, data: data.mobile, borderColor: '#16a34a', backgroundColor: 'rgba(22,163,74,.2)', tension: 0.25, fill: true}
     161                            // Total Visitors
     162                            {
     163                                label: 'Visitors',
     164                                data: data.total_visitors,
     165                                backgroundColor: 'rgba(249, 115, 22, 0.1)',
     166                                borderColor: 'rgb(249, 115, 22)',
     167                                borderWidth: 3,
     168                                tension: 0.4,
     169                                fill: true,
     170                                pointRadius: 4,
     171                                pointHoverRadius: 6,
     172                                pointBackgroundColor: 'rgb(249, 115, 22)',
     173                                pointBorderColor: '#fff',
     174                                pointBorderWidth: 2,
     175                                order: 1
     176                            },
     177                            // Desktop metrics
     178                            {
     179                                label: desktopLabel + ' Visitors',
     180                                data: data.desktop_visitors,
     181                                backgroundColor: 'rgba(59, 130, 246, 0.1)',
     182                                borderColor: 'rgb(59, 130, 246)',
     183                                borderWidth: 3,
     184                                tension: 0.4,
     185                                fill: true,
     186                                pointRadius: 4,
     187                                pointHoverRadius: 6,
     188                                pointBackgroundColor: 'rgb(59, 130, 246)',
     189                                pointBorderColor: '#fff',
     190                                pointBorderWidth: 2,
     191                                order: 2
     192                            },
     193                            {
     194                                label: desktopLabel + ' Events',
     195                                data: data.desktop_events,
     196                                backgroundColor: 'rgba(239, 68, 68, 0.1)',
     197                                borderColor: 'rgb(239, 68, 68)',
     198                                borderWidth: 3,
     199                                tension: 0.4,
     200                                fill: true,
     201                                pointRadius: 4,
     202                                pointHoverRadius: 6,
     203                                pointBackgroundColor: 'rgb(239, 68, 68)',
     204                                pointBorderColor: '#fff',
     205                                pointBorderWidth: 2,
     206                                order: 3
     207                            },
     208                            // Mobile metrics
     209                            {
     210                                label: mobileLabel + ' Visitors',
     211                                data: data.mobile_visitors,
     212                                backgroundColor: 'rgba(34, 197, 94, 0.1)',
     213                                borderColor: 'rgb(34, 197, 94)',
     214                                borderWidth: 3,
     215                                tension: 0.4,
     216                                fill: true,
     217                                pointRadius: 4,
     218                                pointHoverRadius: 6,
     219                                pointBackgroundColor: 'rgb(34, 197, 94)',
     220                                pointBorderColor: '#fff',
     221                                pointBorderWidth: 2,
     222                                order: 4
     223                            },
     224                            {
     225                                label: mobileLabel + ' Events',
     226                                data: data.mobile_events,
     227                                backgroundColor: 'rgba(168, 85, 247, 0.1)',
     228                                borderColor: 'rgb(168, 85, 247)',
     229                                borderWidth: 3,
     230                                tension: 0.4,
     231                                fill: true,
     232                                pointRadius: 4,
     233                                pointHoverRadius: 6,
     234                                pointBackgroundColor: 'rgb(168, 85, 247)',
     235                                pointBorderColor: '#fff',
     236                                pointBorderWidth: 2,
     237                                order: 5
     238                            }
    152239                        ]
    153240                    },
    154241                    options: {
    155242                        responsive: false,
    156                         animation: false,
    157                         plugins: {legend: {display: true}, tooltip: {enabled: true}},
    158                         interaction: {mode: 'nearest', intersect: false},
    159                         scales: {y: {beginAtZero: true, ticks: {precision: 0}}}
     243                        animation: {
     244                            duration: 750,
     245                            easing: 'easeInOutQuart'
     246                        },
     247                        plugins: {
     248                            legend: {
     249                                display: true,
     250                                position: 'top',
     251                                labels: {
     252                                    boxWidth: 12,
     253                                    boxHeight: 12,
     254                                    padding: 10,
     255                                    font: {
     256                                        size: 11,
     257                                        weight: '600'
     258                                    },
     259                                    usePointStyle: true,
     260                                    pointStyle: 'rect'
     261                                }
     262                            },
     263                            tooltip: {
     264                                enabled: true,
     265                                mode: 'index',
     266                                intersect: false,
     267                                backgroundColor: 'rgba(0, 0, 0, 0.9)',
     268                                titleColor: '#fff',
     269                                bodyColor: '#fff',
     270                                borderColor: 'rgba(255, 255, 255, 0.2)',
     271                                borderWidth: 1,
     272                                padding: 12,
     273                                displayColors: true,
     274                                callbacks: {
     275                                    title: function(context) {
     276                                        return context[0].label || '';
     277                                    },
     278                                    label: function(context) {
     279                                        var label = context.dataset.label || '';
     280                                        var value = context.parsed.y || 0;
     281                                        return label + ': ' + fmt(value);
     282                                    }
     283                                }
     284                            }
     285                        },
     286                        interaction: {
     287                            mode: 'index',
     288                            intersect: false
     289                        },
     290                        scales: {
     291                            x: {
     292                                grid: {
     293                                    display: false
     294                                },
     295                                ticks: {
     296                                    font: {
     297                                        size: 10
     298                                    },
     299                                    maxRotation: 45,
     300                                    minRotation: 0
     301                                }
     302                            },
     303                            y: {
     304                                beginAtZero: true,
     305                                grid: {
     306                                    color: 'rgba(0, 0, 0, 0.05)',
     307                                    drawBorder: false
     308                                },
     309                                ticks: {
     310                                    precision: 0,
     311                                    font: {
     312                                        size: 10
     313                                    },
     314                                    callback: function(value) {
     315                                        return fmt(value);
     316                                    }
     317                                }
     318                            }
     319                        }
    160320                    }
    161321                });
     
    179339        });
    180340
    181         // Load referrer data
    182         loadReferrerData();
    183 
    184         // Load outbound click data
    185         loadOutboundClickData();
     341        // Load secondary data with staggered delays for better performance
     342        // This prevents all AJAX requests from firing simultaneously
     343        setTimeout(loadReferrerData, 100);
     344        setTimeout(loadOutboundClickData, 150);
     345        setTimeout(loadCountriesData, 200);
     346        setTimeout(loadBrowsersData, 250);
     347        setTimeout(loadDevicesData, 300);
    186348
    187349        function loadReferrerData() {
     
    318480            });
    319481        }
    320     });
     482
     483        // Helper function to get country ISO code from country name
     484        function getCountryISO(countryName, countryCode) {
     485            var countryToISO = {
     486                'United States': 'us', 'US': 'us',
     487                'Germany': 'de', 'DE': 'de',
     488                'United Kingdom': 'gb', 'GB': 'gb', 'UK': 'gb',
     489                'France': 'fr', 'FR': 'fr',
     490                'India': 'in', 'IN': 'in',
     491                'Netherlands': 'nl', 'NL': 'nl',
     492                'China': 'cn', 'CN': 'cn',
     493                'Canada': 'ca', 'CA': 'ca',
     494                'Italy': 'it', 'IT': 'it',
     495                'Poland': 'pl', 'PL': 'pl',
     496                'Spain': 'es', 'ES': 'es',
     497                'Brazil': 'br', 'BR': 'br',
     498                'Australia': 'au', 'AU': 'au',
     499                'Japan': 'jp', 'JP': 'jp',
     500                'Russia': 'ru', 'RU': 'ru',
     501                'Mexico': 'mx', 'MX': 'mx',
     502                'South Korea': 'kr', 'KR': 'kr',
     503                'Indonesia': 'id', 'ID': 'id',
     504                'Turkey': 'tr', 'TR': 'tr',
     505                'Saudi Arabia': 'sa', 'SA': 'sa',
     506                'Switzerland': 'ch', 'CH': 'ch',
     507                'Belgium': 'be', 'BE': 'be',
     508                'Sweden': 'se', 'SE': 'se',
     509                'Norway': 'no', 'NO': 'no',
     510                'Denmark': 'dk', 'DK': 'dk',
     511                'Finland': 'fi', 'FI': 'fi',
     512                'Austria': 'at', 'AT': 'at',
     513                'Portugal': 'pt', 'PT': 'pt',
     514                'Greece': 'gr', 'GR': 'gr',
     515                'Ireland': 'ie', 'IE': 'ie',
     516                'Thailand': 'th', 'TH': 'th',
     517                'South Africa': 'za', 'ZA': 'za',
     518                'Unknown': 'xx'
     519            };
     520
     521            // If country code is already a 2-letter ISO code, use it
     522            if (countryCode && countryCode.length === 2 && /^[A-Z]{2}$/i.test(countryCode)) {
     523                return countryCode.toLowerCase();
     524            }
     525
     526            // Otherwise, look up the country name
     527            return countryToISO[countryName] || countryToISO[countryCode] || 'xx';
     528        }
     529
     530        function loadCountriesData() {
     531            fetch(ajax, {
     532                method: 'POST',
     533                headers: {'Content-Type': 'application/x-www-form-urlencoded'},
     534                body: 'action=optibehavior_post_countries&post_id=' + encodeURIComponent(pid) + '&nonce=' + encodeURIComponent(nonce)
     535            })
     536            .then(function(r) { return r.json(); })
     537            .then(function(resp) {
     538                var table = document.getElementById('optibehavior-countries-table');
     539                var empty = document.getElementById('optibehavior-countries-empty');
     540
     541                if (resp && resp.success && resp.data && resp.data.length > 0) {
     542                    // Calculate total for percentage
     543                    var total = 0;
     544                    resp.data.forEach(function(item) {
     545                        total += item.count || 0;
     546                    });
     547
     548                    var html = '<table class="countries-table" style="width:100%;border-collapse:collapse;"><tbody>';
     549                    resp.data.forEach(function(item) {
     550                        var countryName = item.country_name || 'Unknown';
     551                        var countryCode = item.country || '';
     552                        var count = item.count || 0;
     553                        var percentage = total > 0 ? Math.round((count / total) * 100) : 0;
     554                        var isoCode = getCountryISO(countryName, countryCode);
     555                        var flagUrl = 'https://flagcdn.com/w40/' + isoCode + '.png';
     556
     557                        html += '<tr style="border-bottom:1px solid #e5e7eb;">';
     558                        html += '<td style="padding:8px 4px;font-size:11px;font-weight:600;color:#1e293b;">';
     559                        html += '<img src="' + flagUrl + '" alt="' + countryName + '" style="width:20px;height:15px;margin-right:8px;vertical-align:middle;border-radius:2px;box-shadow:0 1px 3px rgba(0,0,0,0.1);">';
     560                        html += countryName;
     561                        html += '</td>';
     562                        html += '<td style="padding:8px 4px;text-align:right;font-size:11px;font-weight:600;">';
     563                        html += '<span style="display:inline-flex;align-items:center;gap:6px;">';
     564                        html += '<span style="background:#10b981;color:#fff;padding:4px 8px;border-radius:12px;font-size:11px;min-width:32px;text-align:center;">' + count + '</span>';
     565                        html += '<span style="color:#64748b;font-size:11px;">' + percentage + '%</span>';
     566                        html += '</span>';
     567                        html += '</td>';
     568                        html += '</tr>';
     569                    });
     570                    html += '</tbody></table>';
     571                    table.innerHTML = html;
     572                    table.style.display = 'block';
     573                    empty.classList.remove('is-visible');
     574                } else {
     575                    empty.classList.add('is-visible');
     576                    table.style.display = 'none';
     577                    // Re-initialize Lucide icons for the empty state
     578                    if (typeof lucide !== 'undefined') {
     579                        lucide.createIcons();
     580                    }
     581                }
     582            })
     583            .catch(function() {
     584                var empty = document.getElementById('optibehavior-countries-empty');
     585                empty.classList.add('is-visible');
     586                // Re-initialize Lucide icons for the empty state
     587                if (typeof lucide !== 'undefined') {
     588                    lucide.createIcons();
     589                }
     590            });
     591        }
     592
     593        // Helper function to get browser icon SVG
     594        function getBrowserIconSVG(browserName) {
     595            var name = browserName.toLowerCase();
     596            var browserIcons = {
     597                'chrome': '<svg viewBox="0 0 24 24" style="width:18px;height:18px;"><circle cx="12" cy="12" r="11" fill="#fff"/><path d="M12 1a11 11 0 1 0 11 11A11 11 0 0 0 12 1zm0 2a9 9 0 0 1 7.87 4.5H12a4.5 4.5 0 0 0-3.9 2.25L4.65 5.4A9 9 0 0 1 12 3z" fill="#DB4437"/><path d="M4.65 5.4l3.45 6.35A4.5 4.5 0 0 0 12 16.5a4.47 4.47 0 0 0 2.25-.6l-3.45 6A9 9 0 0 1 4.65 5.4z" fill="#0F9D58"/><path d="M19.87 7.5A9 9 0 0 1 10.8 21.9l3.45-6A4.5 4.5 0 0 0 16.5 12a4.47 4.47 0 0 0-.6-2.25z" fill="#F4B400"/><circle cx="12" cy="12" r="4.5" fill="#4285F4"/><circle cx="12" cy="12" r="3" fill="#fff"/></svg>',
     598                'firefox': '<svg viewBox="0 0 24 24" style="width:18px;height:18px;"><defs><radialGradient id="ffGrad1"><stop offset="0%" stop-color="#FFBD4F"/><stop offset="100%" stop-color="#FF9640"/></radialGradient><radialGradient id="ffGrad2"><stop offset="0%" stop-color="#FF9640"/><stop offset="100%" stop-color="#E63950"/></radialGradient></defs><circle cx="12" cy="12" r="11" fill="url(#ffGrad1)"/><path d="M12 2C8 2 5 4 4 7c0 0 2-1 4-1 0-2 2-3 4-3 3 0 5 2 5 5 0 2-1 3-2 4-2 1-3 2-3 4v1h3v-1c0-1 1-2 2-3 2-1 3-3 3-5 0-4-3-7-8-7z" fill="url(#ffGrad2)"/><ellipse cx="12" cy="19" rx="2" ry="2.5" fill="#fff"/></svg>',
     599                'safari': '<svg viewBox="0 0 24 24" style="width:18px;height:18px;"><defs><linearGradient id="safGrad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#1AC8FC"/><stop offset="100%" stop-color="#0D66D0"/></linearGradient></defs><circle cx="12" cy="12" r="11" fill="url(#safGrad)"/><circle cx="12" cy="12" r="9" fill="none" stroke="#fff" stroke-width="0.5"/><path d="M12 3v2M12 19v2M3 12h2M19 12h2" stroke="#fff" stroke-width="0.8"/><path d="M12 12L8 16" fill="#fff"/><path d="M12 12L16 8" fill="#E63950"/><polygon points="12,12 8,16 10,14 12,12" fill="#fff"/><polygon points="12,12 16,8 14,10 12,12" fill="#E63950"/></svg>',
     600                'edge': '<svg viewBox="0 0 24 24" style="width:18px;height:18px;"><defs><linearGradient id="edgeGrad1" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#0078D4"/><stop offset="100%" stop-color="#1490DF"/></linearGradient></defs><path d="M3 12c0-2 1-4 2-5 2-2 5-4 9-4 3 0 5 1 7 3-1-2-3-3-6-3-5 0-9 4-9 9 0 3 1 5 3 7 1 1 3 2 5 2h1c-3 0-6-1-8-3-2-2-4-4-4-6z" fill="url(#edgeGrad1)"/><path d="M21 9c1 1 2 3 2 5 0 5-4 9-9 9-2 0-4-1-6-2 2 1 4 2 6 2 5 0 9-4 9-9 0-2-1-4-2-5z" fill="#2DCCFF"/><path d="M12 7c-3 0-5 2-5 5h10c0-3-2-5-5-5z" fill="#0078D4"/></svg>',
     601                'opera': '<svg viewBox="0 0 24 24" style="width:18px;height:18px;"><defs><radialGradient id="opGrad"><stop offset="0%" stop-color="#FF1B2D"/><stop offset="100%" stop-color="#A02020"/></radialGradient></defs><circle cx="12" cy="12" r="11" fill="url(#opGrad)"/><ellipse cx="12" cy="12" rx="5" ry="8" fill="none" stroke="#fff" stroke-width="1.5"/></svg>',
     602                'brave': '<svg viewBox="0 0 24 24" style="width:18px;height:18px;"><defs><linearGradient id="braveGrad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#FB542B"/><stop offset="100%" stop-color="#CD3A1F"/></linearGradient></defs><circle cx="12" cy="12" r="11" fill="url(#braveGrad)"/><path d="M12 4l-1 3-2-1-1 3-3 1 1 2-2 2 3 1v3l2-1 2 1v-3l3-1-2-2 1-2-3-1-1-3z" fill="#fff"/></svg>',
     603                'samsung': '<svg viewBox="0 0 24 24" style="width:18px;height:18px;"><defs><linearGradient id="samsungGrad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#7B4FFF"/><stop offset="100%" stop-color="#5A2FD6"/></linearGradient></defs><circle cx="12" cy="12" r="11" fill="url(#samsungGrad)"/><path d="M8 8h8v8H8z" fill="none" stroke="#fff" stroke-width="1.5" rx="1"/><circle cx="12" cy="12" r="3" fill="#fff"/></svg>',
     604                'android': '<svg viewBox="0 0 24 24" style="width:18px;height:18px;"><defs><linearGradient id="androidGrad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#A4C639"/><stop offset="100%" stop-color="#7FA82E"/></linearGradient></defs><circle cx="12" cy="12" r="11" fill="url(#androidGrad)"/><path d="M8 9h8v6c0 1-1 2-2 2h-4c-1 0-2-1-2-2V9z" fill="#fff"/><circle cx="10" cy="11" r="0.8" fill="url(#androidGrad)"/><circle cx="14" cy="11" r="0.8" fill="url(#androidGrad)"/><path d="M9 7l-1-2M15 7l1-2" stroke="#fff" stroke-width="1" stroke-linecap="round"/></svg>'
     605            };
     606
     607            // Check if we have a predefined icon for this browser
     608            for (var key in browserIcons) {
     609                if (name.indexOf(key) !== -1) {
     610                    return browserIcons[key];
     611                }
     612            }
     613
     614            // Default browser icon
     615            return '<svg viewBox="0 0 24 24" style="width:18px;height:18px;"><defs><linearGradient id="defGrad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#9CA3AF"/><stop offset="100%" stop-color="#6B7280"/></linearGradient></defs><circle cx="12" cy="12" r="11" fill="url(#defGrad)"/><circle cx="12" cy="12" r="8" fill="none" stroke="#fff" stroke-width="0.5"/><path d="M12 4v3M12 17v3M4 12h3M17 12h3" stroke="#fff" stroke-width="1"/><circle cx="12" cy="12" r="2" fill="#fff"/></svg>';
     616        }
     617
     618        function loadBrowsersData() {
     619            fetch(ajax, {
     620                method: 'POST',
     621                headers: {'Content-Type': 'application/x-www-form-urlencoded'},
     622                body: 'action=optibehavior_post_browsers&post_id=' + encodeURIComponent(pid) + '&nonce=' + encodeURIComponent(nonce)
     623            })
     624            .then(function(r) { return r.json(); })
     625            .then(function(resp) {
     626                var table = document.getElementById('optibehavior-browsers-table');
     627                var empty = document.getElementById('optibehavior-browsers-empty');
     628
     629                if (resp && resp.success && resp.data && resp.data.length > 0) {
     630                    // Calculate total for percentage
     631                    var total = 0;
     632                    resp.data.forEach(function(item) {
     633                        total += item.count || 0;
     634                    });
     635
     636                    var html = '<table class="browsers-table" style="width:100%;border-collapse:collapse;"><tbody>';
     637                    resp.data.forEach(function(item) {
     638                        var browserName = item.name || 'Unknown';
     639                        var count = item.count || 0;
     640                        var percentage = total > 0 ? Math.round((count / total) * 100) : 0;
     641                        var browserIconSVG = getBrowserIconSVG(browserName);
     642
     643                        html += '<tr style="border-bottom:1px solid #e5e7eb;">';
     644                        html += '<td style="padding:8px 4px;font-size:11px;font-weight:600;color:#1e293b;">';
     645                        html += '<span style="display:inline-flex;align-items:center;gap:8px;">';
     646                        html += '<span style="display:inline-block;vertical-align:middle;">' + browserIconSVG + '</span>';
     647                        html += '<span>' + browserName + '</span>';
     648                        html += '</span>';
     649                        html += '</td>';
     650                        html += '<td style="padding:8px 4px;text-align:right;font-size:11px;font-weight:600;">';
     651                        html += '<span style="display:inline-flex;align-items:center;gap:6px;">';
     652                        html += '<span style="background:#10b981;color:#fff;padding:4px 8px;border-radius:12px;font-size:11px;min-width:32px;text-align:center;">' + count + '</span>';
     653                        html += '<span style="color:#64748b;font-size:11px;">' + percentage + '%</span>';
     654                        html += '</span>';
     655                        html += '</td>';
     656                        html += '</tr>';
     657                    });
     658                    html += '</tbody></table>';
     659                    table.innerHTML = html;
     660                    table.style.display = 'block';
     661                    empty.classList.remove('is-visible');
     662                } else {
     663                    empty.classList.add('is-visible');
     664                    table.style.display = 'none';
     665                    // Re-initialize Lucide icons for the empty state
     666                    if (typeof lucide !== 'undefined') {
     667                        lucide.createIcons();
     668                    }
     669                }
     670            })
     671            .catch(function() {
     672                var empty = document.getElementById('optibehavior-browsers-empty');
     673                empty.classList.add('is-visible');
     674                // Re-initialize Lucide icons for the empty state
     675                if (typeof lucide !== 'undefined') {
     676                    lucide.createIcons();
     677                }
     678            });
     679        }
     680
     681        // Helper function to get device icon SVG
     682        function getDeviceIconSVG(deviceName) {
     683            var name = deviceName.toLowerCase();
     684
     685            if (name.indexOf('desktop') !== -1) {
     686                return '<svg viewBox="0 0 24 24" style="width:18px;height:18px;"><defs><linearGradient id="desktopGrad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#3B82F6"/><stop offset="100%" stop-color="#2563EB"/></linearGradient></defs><rect x="2" y="4" width="20" height="12" rx="1" fill="url(#desktopGrad)"/><rect x="4" y="6" width="16" height="8" fill="#EFF6FF"/><rect x="9" y="16" width="6" height="1" fill="url(#desktopGrad)"/><rect x="7" y="17" width="10" height="2" rx="0.5" fill="url(#desktopGrad)"/></svg>';
     687            } else if (name.indexOf('mobile') !== -1 || name.indexOf('phone') !== -1) {
     688                return '<svg viewBox="0 0 24 24" style="width:18px;height:18px;"><defs><linearGradient id="mobileGrad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#10B981"/><stop offset="100%" stop-color="#059669"/></linearGradient></defs><rect x="6" y="2" width="12" height="20" rx="2" fill="url(#mobileGrad)"/><rect x="7" y="4" width="10" height="14" fill="#ECFDF5"/><circle cx="12" cy="20" r="1" fill="#ECFDF5"/></svg>';
     689            } else if (name.indexOf('tablet') !== -1) {
     690                return '<svg viewBox="0 0 24 24" style="width:18px;height:18px;"><defs><linearGradient id="tabletGrad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#F59E0B"/><stop offset="100%" stop-color="#D97706"/></linearGradient></defs><rect x="4" y="2" width="16" height="20" rx="2" fill="url(#tabletGrad)"/><rect x="5" y="4" width="14" height="15" fill="#FEF3C7"/><circle cx="12" cy="20.5" r="1" fill="#FEF3C7"/></svg>';
     691            }
     692
     693            // Default device icon
     694            return '<svg viewBox="0 0 24 24" style="width:18px;height:18px;"><defs><linearGradient id="devDefGrad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#9CA3AF"/><stop offset="100%" stop-color="#6B7280"/></linearGradient></defs><rect x="2" y="4" width="20" height="12" rx="1" fill="url(#devDefGrad)"/><rect x="4" y="6" width="16" height="8" fill="#F3F4F6"/></svg>';
     695        }
     696
     697        function loadDevicesData() {
     698            fetch(ajax, {
     699                method: 'POST',
     700                headers: {'Content-Type': 'application/x-www-form-urlencoded'},
     701                body: 'action=optibehavior_post_devices&post_id=' + encodeURIComponent(pid) + '&nonce=' + encodeURIComponent(nonce)
     702            })
     703            .then(function(r) { return r.json(); })
     704            .then(function(resp) {
     705                var table = document.getElementById('optibehavior-devices-table');
     706                var empty = document.getElementById('optibehavior-devices-empty');
     707
     708                if (resp && resp.success && resp.data && resp.data.length > 0) {
     709                    // Calculate total for percentage
     710                    var total = 0;
     711                    resp.data.forEach(function(item) {
     712                        total += item.count || 0;
     713                    });
     714
     715                    var html = '<table class="devices-table" style="width:100%;border-collapse:collapse;"><tbody>';
     716                    resp.data.forEach(function(item) {
     717                        var deviceName = item.name || 'Unknown';
     718                        var count = item.count || 0;
     719                        var percentage = total > 0 ? Math.round((count / total) * 100) : 0;
     720                        var deviceIconSVG = getDeviceIconSVG(deviceName);
     721
     722                        html += '<tr style="border-bottom:1px solid #e5e7eb;">';
     723                        html += '<td style="padding:8px 4px;font-size:11px;font-weight:600;color:#1e293b;">';
     724                        html += '<span style="display:inline-flex;align-items:center;gap:8px;">';
     725                        html += '<span style="display:inline-block;vertical-align:middle;">' + deviceIconSVG + '</span>';
     726                        html += '<span>' + deviceName + '</span>';
     727                        html += '</span>';
     728                        html += '</td>';
     729                        html += '<td style="padding:8px 4px;text-align:right;font-size:11px;font-weight:600;">';
     730                        html += '<span style="display:inline-flex;align-items:center;gap:6px;">';
     731                        html += '<span style="background:#10b981;color:#fff;padding:4px 8px;border-radius:12px;font-size:11px;min-width:32px;text-align:center;">' + count + '</span>';
     732                        html += '<span style="color:#64748b;font-size:11px;">' + percentage + '%</span>';
     733                        html += '</span>';
     734                        html += '</td>';
     735                        html += '</tr>';
     736                    });
     737                    html += '</tbody></table>';
     738                    table.innerHTML = html;
     739                    table.style.display = 'block';
     740                    empty.classList.remove('is-visible');
     741                } else {
     742                    empty.classList.add('is-visible');
     743                    table.style.display = 'none';
     744                    // Re-initialize Lucide icons for the empty state
     745                    if (typeof lucide !== 'undefined') {
     746                        lucide.createIcons();
     747                    }
     748                }
     749            })
     750            .catch(function() {
     751                var empty = document.getElementById('optibehavior-devices-empty');
     752                empty.classList.add('is-visible');
     753                // Re-initialize Lucide icons for the empty state
     754                if (typeof lucide !== 'undefined') {
     755                    lucide.createIcons();
     756                }
     757            });
     758        }
     759    }
     760
     761    // Initialize as soon as possible - try immediately, then on DOMContentLoaded, then on load
     762    if (document.readyState === 'loading') {
     763        // DOM is still loading, wait for DOMContentLoaded
     764        document.addEventListener('DOMContentLoaded', initMetaboxAnalytics);
     765    } else {
     766        // DOM is already loaded, initialize immediately
     767        initMetaboxAnalytics();
     768    }
    321769})();
    322770
  • opti-behavior/trunk/assets/js/opti-behavior-heatmap-simple.js

    r3399182 r3401441  
    1717   
    1818    function init() {
     19        console.log('[Heatmap] init() called');
     20
    1921        // Check if we have the required configuration
    2022        if (typeof window.opti_behavior_heatmap === 'undefined') {
    21             // console.error('[Heatmap] Configuration not found');
     23            console.error('[Heatmap] Configuration not found');
    2224            return;
    2325        }
    24        
     26
     27        console.log('[Heatmap] Config found:', window.opti_behavior_heatmap);
     28
    2529        const config = window.opti_behavior_heatmap;
    26        
     30
    2731        // Only initialize in reporter mode
    2832        if (config._mode !== 'reporter') {
     33            console.log('[Heatmap] Not in reporter mode, mode is:', config._mode);
    2934            return;
    3035        }
    31        
     36
     37        console.log('[Heatmap] Reporter mode confirmed');
     38
    3239        // Check if click tracking is enabled
    3340        const reports = Array.isArray(config.reports) ? config.reports : config.reports.split(',');
    3441        const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
    3542        const clickEvent = isMobile ? 'click_mobile' : 'click_pc';
    36        
     43
     44        console.log('[Heatmap] Reports:', reports);
     45        console.log('[Heatmap] isMobile:', isMobile);
     46        console.log('[Heatmap] Looking for event:', clickEvent);
     47        console.log('[Heatmap] Event included?', reports.includes(clickEvent));
     48
    3749        if (!reports.includes(clickEvent)) {
    38             // if (config.debug) {
    39             //     console.log('[Heatmap] Click tracking not enabled for this device type');
    40             // }
     50            console.log('[Heatmap] Click tracking not enabled for this device type');
    4151            return;
    4252        }
    43        
     53
     54        console.log('[Heatmap] Initializing tracker...');
     55
    4456        // Initialize click tracking
    4557        const tracker = new HeatmapTracker(config);
    4658        tracker.start();
    4759
    48         // if (config.debug) {
    49         //     console.log('[Heatmap] Tracker initialized', config);
    50         // }
     60        console.log('[Heatmap] Tracker initialized', config);
    5161    }
    5262   
     
    6272    }
    6373   
     74    /**
     75     * Get cookie value by name
     76     */
     77    HeatmapTracker.prototype.getCookie = function(name) {
     78        const value = `; ${document.cookie}`;
     79        const parts = value.split(`; ${name}=`);
     80        if (parts.length === 2) {
     81            return parts.pop().split(';').shift();
     82        }
     83        return null;
     84    };
     85
    6486    HeatmapTracker.prototype.start = function() {
     87        console.log('[Heatmap] start() called');
     88
    6589        // Attach click event listener
    6690        document.addEventListener('click', this.handleClick.bind(this), true);
    67        
     91
     92        // Attach outbound link click tracker
     93        document.addEventListener('click', this.handleOutboundClick.bind(this), true);
     94
    6895        // Start periodic send timer
    6996        this.startSendTimer();
    70        
     97
    7198        // Send data before page unload
    7299        window.addEventListener('beforeunload', this.sendData.bind(this, true));
    73100
    74         // if (this.config.debug) {
    75         //     console.log('[Heatmap] Click tracking started');
    76         // }
     101        console.log('[Heatmap] Click tracking started');
    77102    };
    78103   
    79104    HeatmapTracker.prototype.handleClick = function(event) {
     105        console.log('[Heatmap] Click detected!', event);
     106
    80107        // Get click coordinates
    81108        const x = event.pageX || (event.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft));
    82109        const y = event.pageY || (event.clientY + (document.documentElement.scrollTop || document.body.scrollTop));
    83        
     110
    84111        // Get page dimensions
    85112        const width = Math.max(
     
    93120            document.documentElement.clientHeight
    94121        );
    95        
     122
    96123        // Create event data
    97124        const eventData = {
     
    104131            time: 0 // Will be calculated relative to send time
    105132        };
    106        
     133
    107134        // Add to queue
    108135        this.eventQueue.push(eventData);
    109136
    110         // if (this.config.debug) {
    111         //     console.log('[Heatmap] Click recorded:', eventData);
    112         // }
     137        console.log('[Heatmap] Click recorded:', eventData);
     138        console.log('[Heatmap] Queue length:', this.eventQueue.length);
    113139
    114140        // Check if we should send immediately
     
    121147        }
    122148    };
    123    
     149
     150    HeatmapTracker.prototype.handleOutboundClick = function(event) {
     151        // Check if click was on a link or within a link
     152        let target = event.target;
     153        let link = null;
     154
     155        // Traverse up the DOM tree to find a link element
     156        while (target && target !== document) {
     157            if (target.tagName === 'A' && target.href) {
     158                link = target;
     159                break;
     160            }
     161            target = target.parentElement;
     162        }
     163
     164        // If no link found, return
     165        if (!link) {
     166            return;
     167        }
     168
     169        // Get the target URL
     170        const targetUrl = link.href;
     171        const sourceUrl = window.location.href;
     172
     173        // Get click coordinates
     174        const x = event.pageX || (event.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft));
     175        const y = event.pageY || (event.clientY + (document.documentElement.scrollTop || document.body.scrollTop));
     176
     177        // Get element details
     178        const elementTag = link.tagName.toLowerCase();
     179        const elementId = link.id || null;
     180        const elementClass = link.className || null;
     181        const elementText = link.textContent ? link.textContent.trim().substring(0, 500) : null;
     182
     183        // Prepare outbound click data
     184        const outboundData = {
     185            type: 'outbound_click',
     186            sourceUrl: sourceUrl,
     187            targetUrl: targetUrl,
     188            elementTag: elementTag,
     189            elementId: elementId,
     190            elementClass: elementClass,
     191            elementText: elementText,
     192            x: Math.floor(x),
     193            y: Math.floor(y)
     194        };
     195
     196        console.log('[Heatmap] Outbound click detected:', outboundData);
     197
     198        // Send outbound click data immediately
     199        this.sendOutboundClick(outboundData);
     200    };
     201
     202    HeatmapTracker.prototype.sendOutboundClick = function(data) {
     203        console.log('[Heatmap] Sending outbound click:', data);
     204
     205        // Get session and visitor IDs from cookies
     206        const sessionId = this.getCookie('optibehavior_sid') || this.getCookie('opti_behavior_session_id') || '';
     207        const visitorId = this.getCookie('optibehavior_vid') || this.getCookie('opti_behavior_visitor_id') || '';
     208
     209        // Prepare JSON data for session recording endpoint
     210        const payload = {
     211            action: 'outbound_click',
     212            session_id: sessionId,
     213            visitor_id: visitorId,
     214            sourceUrl: data.sourceUrl,
     215            targetUrl: data.targetUrl,
     216            elementTag: data.elementTag,
     217            elementId: data.elementId || null,
     218            elementClass: data.elementClass || null,
     219            elementText: data.elementText || null,
     220            x: data.x,
     221            y: data.y
     222        };
     223
     224        // Prepare form data for session recording endpoint
     225        const formData = new FormData();
     226        formData.append('action', 'opti_behavior_heatmap_record');
     227        formData.append('nonce', this.config.nonce);
     228        formData.append('data', JSON.stringify(payload));
     229
     230        console.log('[Heatmap] Outbound click payload:', payload);
     231
     232        // Use sendBeacon for outbound clicks (more reliable when navigating away)
     233        // Fallback to fetch if sendBeacon is not supported
     234        if (navigator.sendBeacon) {
     235            try {
     236                const sent = navigator.sendBeacon(this.config.ajax_url, formData);
     237                console.log('[Heatmap] Outbound click sent via sendBeacon:', sent);
     238            } catch(e) {
     239                console.error('[Heatmap] sendBeacon error, falling back to fetch:', e);
     240                // Fallback to fetch
     241                fetch(this.config.ajax_url, {
     242                    method: 'POST',
     243                    body: formData,
     244                    mode: 'same-origin',
     245                    cache: 'no-cache',
     246                    keepalive: true
     247                }).catch(function(error) {
     248                    console.error('[Heatmap] Outbound click fetch error:', error);
     249                });
     250            }
     251        } else {
     252            // Fallback to fetch with keepalive
     253            fetch(this.config.ajax_url, {
     254                method: 'POST',
     255                body: formData,
     256                mode: 'same-origin',
     257                cache: 'no-cache',
     258                keepalive: true
     259            }).then(function(response) {
     260                console.log('[Heatmap] Outbound click sent via fetch, status:', response.status);
     261                return response.json();
     262            }).then(function(responseData) {
     263                console.log('[Heatmap] Outbound click response:', responseData);
     264            }).catch(function(error) {
     265                console.error('[Heatmap] Outbound click error:', error);
     266            });
     267        }
     268    };
     269
    124270    HeatmapTracker.prototype.startSendTimer = function() {
    125271        // Clear existing timer
     
    137283   
    138284    HeatmapTracker.prototype.sendData = function(isUnload) {
     285        console.log('[Heatmap] sendData() called, isUnload:', isUnload);
     286        console.log('[Heatmap] Queue length:', this.eventQueue.length);
     287
    139288        if (this.eventQueue.length === 0) {
     289            console.log('[Heatmap] No events to send');
    140290            return;
    141291        }
     
    144294        const eventsToSend = this.eventQueue.splice(0);
    145295
     296        console.log('[Heatmap] Sending events:', eventsToSend);
     297
    146298        // Update last send time
    147299        this.lastSendTime = Date.now();
     300
     301        // Get session and visitor IDs from cookies
     302        const sessionId = this.getCookie('optibehavior_sid') || this.getCookie('opti_behavior_session_id');
     303        const visitorId = this.getCookie('optibehavior_vid') || this.getCookie('opti_behavior_visitor_id');
     304
     305        console.log('[Heatmap] Session ID:', sessionId);
     306        console.log('[Heatmap] Visitor ID:', visitorId);
    148307
    149308        // Prepare form data
     
    153312        formData.append('url', window.location.href);
    154313        formData.append('title', document.title);
     314
     315        // Add session and visitor IDs if available
     316        if (sessionId) {
     317            formData.append('session_id', sessionId);
     318        }
     319        if (visitorId) {
     320            formData.append('visitor_id', visitorId);
     321        }
     322
     323        console.log('[Heatmap] AJAX config:', {
     324            action: this.config.action,
     325            nonce: this.config.nonce,
     326            ajax_url: this.config.ajax_url,
     327            url: window.location.href,
     328            title: document.title
     329        });
    155330
    156331        // Add screen resolution data for visitor tracking
     
    212387        if (isUnload && navigator.sendBeacon) {
    213388            // Use sendBeacon for unload events
     389            console.log('[Heatmap] Using sendBeacon');
    214390            navigator.sendBeacon(this.config.ajax_url, formData);
    215391        } else {
    216392            // Use fetch for normal sends
     393            console.log('[Heatmap] Using fetch to send data');
    217394            fetch(this.config.ajax_url, {
    218395                method: 'POST',
     
    222399                keepalive: isUnload
    223400            }).then(function(response) {
    224                 // if (this.config.debug) {
    225                 //     console.log('[Heatmap] Server response:', response.status);
    226                 // }
     401                console.log('[Heatmap] Server response status:', response.status);
    227402                return response.json();
    228403            }.bind(this)).then(function(data) {
    229                 // if (this.config.debug) {
    230                 //     console.log('[Heatmap] Server data:', data);
    231                 // }
     404                console.log('[Heatmap] Server response data:', data);
    232405            }.bind(this)).catch(function(error) {
    233                 // console.error('[Heatmap] Send error:', error);
     406                console.error('[Heatmap] Send error:', error);
    234407            });
    235408        }
  • opti-behavior/trunk/includes/class-opti-behavior-heatmap-database.php

    r3399182 r3401441  
    334334            id           varchar(64)          NOT NULL,
    335335            visitor_id   varchar(64)          NOT NULL,
     336            user_id      bigint(20)  UNSIGNED DEFAULT NULL,
    336337            start_time   datetime             NOT NULL,
    337338            end_time     datetime             DEFAULT NULL,
     
    354355            PRIMARY KEY  (id),
    355356            KEY visitor_id (visitor_id),
     357            KEY user_id (user_id),
    356358            KEY start_time (start_time),
    357359            KEY is_bounce (is_bounce),
  • opti-behavior/trunk/includes/class-opti-behavior-license-manager.php

    r3399182 r3401441  
    1616    /**
    1717     * API base URL
    18      * TODO: Replace with production API URL before release
    19      */
    20     const API_URL = 'https://api.opti-behavior.com/v1/';
     18     */
     19    const API_URL = 'https://api.opti-behavior.com/';
    2120   
    2221    /**
     
    115114        update_option( self::OPTION_REGISTRATION_DATE, current_time( 'mysql' ) );
    116115
     116        // Store master_secret for API authentication (encrypted)
     117        if ( isset( $response['data']['master_secret'] ) ) {
     118            $encrypted_master_secret = $this->encrypt_secret_key( $response['data']['master_secret'] );
     119            update_option( 'opti_behavior_master_secret', $response['data']['master_secret'] );
     120            $this->debug_manager->log( 'Master secret stored for API authentication', 'info', 'license' );
     121        }
     122
    117123        // Clear sensitive data from memory (only if native extension is available)
    118124        if ( extension_loaded( 'sodium' ) ) {
     
    220226    public function call_api( $endpoint, $data = array(), $method = 'GET' ) {
    221227        $url = self::API_URL . $endpoint;
     228        $installation_id = $this->get_installation_id();
     229        $timestamp = time();
     230
     231        // Prepare request data for signature
     232        if ( $method === 'POST' ) {
     233            $data_to_sign = wp_json_encode( $data, JSON_UNESCAPED_SLASHES );
     234        } else {
     235            // Sort GET parameters for consistent signing
     236            ksort( $data );
     237            $data_to_sign = wp_json_encode( $data, JSON_UNESCAPED_SLASHES );
     238        }
     239
     240        // Generate HMAC signature (only if registered)
     241        $signature = '';
     242        if ( ! empty( $installation_id ) && $endpoint !== 'register' ) {
     243            $signature = $this->generate_api_signature( $data_to_sign, $timestamp );
     244        }
    222245
    223246        $args = array(
     
    225248            'headers' => array(
    226249                'Content-Type' => 'application/json',
     250                'X-OptiBe-Installation-ID' => $installation_id,
     251                'X-OptiBe-Signature' => $signature,
     252                'X-OptiBe-Timestamp' => $timestamp,
    227253            ),
    228254        );
     
    247273        return $decoded;
    248274    }
     275
     276    /**
     277     * Generate HMAC signature for API request
     278     *
     279     * @param string $data Request data (JSON string)
     280     * @param int $timestamp Request timestamp
     281     * @return string HMAC-SHA256 signature
     282     */
     283    private function generate_api_signature( $data, $timestamp ) {
     284        $master_secret = get_option( 'opti_behavior_master_secret', '' );
     285
     286        if ( empty( $master_secret ) ) {
     287            $this->debug_manager->log( 'Master secret not found - cannot sign request', 'warning', 'license' );
     288            return '';
     289        }
     290
     291        // Normalize data
     292        $decoded = json_decode( $data, true );
     293        if ( $decoded !== null && is_array( $decoded ) ) {
     294            ksort( $decoded );
     295            $normalized = json_encode( $decoded, JSON_UNESCAPED_SLASHES );
     296        } else {
     297            $normalized = trim( $data );
     298        }
     299
     300        // Generate signature: HMAC-SHA256(data|timestamp, master_secret)
     301        $message = $normalized . '|' . $timestamp;
     302        return hash_hmac( 'sha256', $message, $master_secret );
     303    }
    249304   
    250305    /**
     
    305360
    306361    /**
    307      * Increment quota usage
    308      *
    309      * This is called after successfully saving a recording
    310      * The API will track the actual usage, this is just a local notification
    311      */
    312     public function increment_quota_usage() {
    313         // The API tracks usage automatically when we request decrypt keys
    314         // This method is here for future local tracking if needed
    315         $this->debug_manager->log( 'Quota usage incremented', 'debug', 'license' );
     362     * Increment quota usage by recording a session
     363     *
     364     * CRITICAL: This is called after successfully saving a recording
     365     * This is the PRIMARY quota enforcement mechanism - prevents bypass attempts
     366     *
     367     * @param string $session_id Session ID (optional, for logging)
     368     * @return bool Success
     369     */
     370    public function increment_quota_usage( $session_id = '' ) {
     371        $installation_id = $this->get_installation_id();
     372
     373        if ( empty( $installation_id ) ) {
     374            $this->debug_manager->log( 'Cannot increment quota: installation_id is empty', 'error', 'license' );
     375            return false;
     376        }
     377
     378        // Call API to increment sessions_recorded counter
     379        $response = $this->call_api( 'record-session', array(
     380            'installation_id' => $installation_id,
     381            'session_id' => $session_id,
     382        ), 'POST' );
     383
     384        if ( ! $response || ! isset( $response['success'] ) || ! $response['success'] ) {
     385            $error_msg = isset( $response['error'] ) ? $response['error'] : 'unknown';
     386            $this->debug_manager->log( 'Failed to increment quota: ' . $error_msg, 'error', 'license' );
     387            return false;
     388        }
     389
     390        $this->debug_manager->log( 'Quota incremented successfully. Session: ' . $session_id, 'info', 'license' );
     391        return true;
    316392    }
    317393}
  • opti-behavior/trunk/includes/trait-opti-behavior-ai-insights-views.php

    r3399182 r3401441  
    175175                        <span class="analyze-text"><?php esc_html_e( 'Revenue Impact', 'opti-behavior' ); ?></span>
    176176                    </div>
    177                 </div>
    178             </div>
    179 
    180             <!-- Feedback Section -->
     177                    <div class="analyze-item">
     178                        <span class="analyze-icon"><i data-lucide="target"></i></span>
     179                        <span class="analyze-text"><?php esc_html_e( 'Conversion Goals', 'opti-behavior' ); ?></span>
     180                    </div>
     181                    <div class="analyze-item">
     182                        <span class="analyze-icon"><i data-lucide="users"></i></span>
     183                        <span class="analyze-text"><?php esc_html_e( 'Audience Segments', 'opti-behavior' ); ?></span>
     184                    </div>
     185                </div>
     186            </div>
     187
     188            <!-- Support Contact Section -->
    181189            <div class="ai-feedback-section">
    182190                <div class="feedback-card">
    183                     <div class="feedback-icon">💬</div>
    184                     <h2 class="feedback-title"><?php esc_html_e( 'Help Shape the Future!', 'opti-behavior' ); ?></h2>
     191                    <div class="feedback-icon">&#128172;</div>
     192                    <h2 class="feedback-title"><?php esc_html_e( 'Need Help or Have Questions?', 'opti-behavior' ); ?></h2>
    185193                    <p class="feedback-description">
    186                         <?php esc_html_e( 'We\'re building this amazing AI-powered feature and we\'d love to hear from you! Your experience and feedback are invaluable in making this the best analytics tool possible.', 'opti-behavior' ); ?>
     194                        <?php esc_html_e( 'We are here to help! Send us a message and our support team will get back to you as soon as possible.', 'opti-behavior' ); ?>
    187195                    </p>
    188                     <div class="feedback-actions">
    189                         <a href="mailto:[email protected]?subject=AI%20Insights%20Feedback" class="feedback-btn primary">
    190                             <span>📧</span>
    191                             <?php esc_html_e( 'Share Your Ideas', 'opti-behavior' ); ?>
    192                         </a>
    193 
    194                     </div>
     196                    <?php
     197                    $current_user = wp_get_current_user();
     198                    $user_email = $current_user->user_email ? $current_user->user_email : '';
     199                    ?>
     200
     201                    <form id="opti-behavior-support-form" class="support-form">
     202                        <div class="form-group">
     203                            <label for="support-name">Your Name</label>
     204                            <input type="text" id="support-name" name="support_name" required class="form-control" />
     205                        </div>
     206
     207                        <div class="form-group">
     208                            <label for="support-email">Your Email</label>
     209                            <input type="email" id="support-email" name="support_email" value="<?php echo esc_attr( $user_email ); ?>" required class="form-control" />
     210                            <small class="form-hint">We will use this to respond to your message</small>
     211                        </div>
     212
     213                        <div class="form-group">
     214                            <label for="support-subject">Subject</label>
     215                            <input type="text" id="support-subject" name="support_subject" required class="form-control" />
     216                        </div>
     217
     218                        <div class="form-group">
     219                            <label for="support-message">Message</label>
     220                            <textarea id="support-message" name="support_message" rows="5" required class="form-control"></textarea>
     221                        </div>
     222
     223                        <div class="form-actions">
     224                            <button type="submit" class="feedback-btn primary">
     225                                <span>&#128231;</span>
     226                                Send Message
     227                            </button>
     228                        </div>
     229
     230                        <div id="opti-support-response" class="support-response support-response-hidden"></div>
     231                    </form>
     232
    195233                    <p class="feedback-note">
    196234                        <?php
    197235                        printf(
    198                             /* translators: %1$s: opening strong tag, %2$s: closing strong tag, %3$s: opening strong tag, %4$s: closing strong tag */
    199                             esc_html__( '%1$sEarly adopters%2$s who share feedback will get %3$sexclusive early access%4$s when we launch! 🎉', 'opti-behavior' ),
    200                             '<strong>',
    201                             '</strong>',
    202                             '<strong>',
    203                             '</strong>'
     236                            /* translators: %s: support email address */
     237                            esc_html__( 'You can also reach us directly at %s', 'opti-behavior' ),
     238                            '<a href="mailto:[email protected]">[email protected]</a>'
    204239                        );
    205240                        ?>
  • opti-behavior/trunk/includes/trait-opti-behavior-ajax-handlers.php

    r3399182 r3401441  
    255255            $sessions_table = esc_sql( $wpdb->prefix . 'optibehavior_sessions' );
    256256            $visitors_table = esc_sql( $wpdb->prefix . 'optibehavior_visitors' );
     257            $users_table = esc_sql( $wpdb->users );
    257258
    258259            // Aggregate engagement metrics per visitor
     
    261262                "SELECT
    262263                    s.visitor_id,
     264                    s.user_id,
    263265                    COUNT(*) AS sessions,
    264266                    COUNT(DISTINCT DATE(s.start_time)) AS active_days,
     
    268270                    MAX(COALESCE(s.end_time, s.start_time)) AS last_seen,
    269271                    UPPER(COALESCE(NULLIF(TRIM(v.country),''), '')) AS country_code,
    270                     v.country_name
     272                    v.country_name,
     273                    u.user_login,
     274                    u.display_name
    271275                 FROM {$sessions_table} s
    272276                 LEFT JOIN {$visitors_table} v ON v.id = s.visitor_id
     277                 LEFT JOIN {$users_table} u ON u.ID = s.user_id
    273278                 WHERE s.start_time BETWEEN %s AND %s
    274                  GROUP BY s.visitor_id, v.country, v.country_name
     279                 GROUP BY s.visitor_id, s.user_id, v.country, v.country_name, u.user_login, u.display_name
    275280                 ORDER BY total_time DESC
    276281                 LIMIT 50",
     
    310315                }
    311316
     317                // Check if this visitor is a WordPress user
     318                $user_id = !empty($r->user_id) ? (int)$r->user_id : 0;
     319                $user_login = !empty($r->user_login) ? (string)$r->user_login : '';
     320                $display_name = !empty($r->display_name) ? (string)$r->display_name : '';
     321                $profile_url = $user_id > 0 ? admin_url('user-edit.php?user_id=' . $user_id) : '';
     322
    312323                $items[] = array(
    313324                    'visitor_id'       => $vid,
    314325                    'visitor_short'    => $short,
     326                    'user_id'          => $user_id,
     327                    'user_login'       => $user_login,
     328                    'display_name'     => $display_name,
     329                    'profile_url'      => $profile_url,
    315330                    'sessions'         => (int)$r->sessions,
    316331                    'pages_per_session'=> (float)$r->pages_per_session,
  • opti-behavior/trunk/includes/trait-opti-behavior-assets.php

    r3399182 r3401441  
    143143                OPTI_BEHAVIOR_HEATMAP_ASSETS_URL . 'js/heatmaps.js',
    144144                array(), // No dependencies
    145                 OPTI_BEHAVIOR_HEATMAP_VERSION,
     145                OPTI_BEHAVIOR_HEATMAP_VERSION . '.' . time(),
    146146                true
    147147            );
     
    155155                array( 'opti-behavior-dashboard' ),
    156156                OPTI_BEHAVIOR_HEATMAP_VERSION
     157            );
     158
     159            // Enqueue support form script
     160            wp_enqueue_script(
     161                'opti-behavior-support-form',
     162                OPTI_BEHAVIOR_HEATMAP_ASSETS_URL . 'js/support-form.js',
     163                array(),
     164                OPTI_BEHAVIOR_HEATMAP_VERSION . '-v3',
     165                true
     166            );
     167
     168            // Pass AJAX URL, nonce, and translated strings to support form script
     169            wp_localize_script(
     170                'opti-behavior-support-form',
     171                'optiBehaviorSupport',
     172                array(
     173                    'ajaxUrl' => admin_url( 'admin-ajax.php' ),
     174                    'nonce'   => wp_create_nonce( 'opti_behavior_support_email' ),
     175                    'i18n'    => array(
     176                        'sending'     => __( 'Sending...', 'opti-behavior' ),
     177                        'sendMessage' => __( 'Send Message', 'opti-behavior' ),
     178                        'error'       => __( 'An error occurred. Please try again.', 'opti-behavior' ),
     179                    ),
     180                )
    157181            );
    158182        }
  • opti-behavior/trunk/includes/trait-opti-behavior-heatmaps-views.php

    r3399182 r3401441  
    5757                            <div class="stat-number"><?php echo esc_html( $stats['total_heatmaps'] ); ?></div>
    5858                            <div class="stat-label"><?php esc_html_e( 'Total Heatmaps', 'opti-behavior' ); ?></div>
    59                             <div class="stat-change positive">+<?php echo esc_html( $stats['heatmaps_growth'] ); ?>% <?php esc_html_e( 'this week', 'opti-behavior' ); ?></div>
     59                            <div class="stat-change <?php echo $stats['heatmaps_growth'] >= 0 ? 'positive' : 'negative'; ?>"><?php
     60                                $change_text = $stats['heatmaps_growth'] > 0 ? '+' . $stats['heatmaps_growth'] : $stats['heatmaps_growth'];
     61                                echo esc_html( $change_text );
     62                            ?>% <?php esc_html_e( 'this week', 'opti-behavior' ); ?></div>
    6063                        </div>
    6164                    </div>
     
    6669                            <div class="stat-number"><?php echo esc_html( number_format( $stats['total_clicks'] ) ); ?></div>
    6770                            <div class="stat-label"><?php esc_html_e( 'Total Clicks', 'opti-behavior' ); ?></div>
    68                             <div class="stat-change positive">+<?php echo esc_html( $stats['clicks_growth'] ); ?>% <?php esc_html_e( 'vs last period', 'opti-behavior' ); ?></div>
     71                            <div class="stat-change <?php echo $stats['clicks_growth'] >= 0 ? 'positive' : 'negative'; ?>"><?php
     72                                $change_text = $stats['clicks_growth'] > 0 ? '+' . $stats['clicks_growth'] : $stats['clicks_growth'];
     73                                echo esc_html( $change_text );
     74                            ?>% <?php esc_html_e( 'this week', 'opti-behavior' ); ?></div>
    6975                        </div>
    7076                    </div>
     
    7581                            <div class="stat-number"><?php echo esc_html( $stats['mobile_percentage'] ); ?>%</div>
    7682                            <div class="stat-label"><?php esc_html_e( 'Mobile Traffic', 'opti-behavior' ); ?></div>
    77                             <div class="stat-change neutral"><?php echo esc_html( $stats['mobile_change'] ); ?>% <?php esc_html_e( 'mobile vs desktop', 'opti-behavior' ); ?></div>
     83                            <div class="stat-change <?php echo $stats['mobile_change'] >= 0 ? 'positive' : 'negative'; ?>"><?php
     84                                $change_text = $stats['mobile_change'] > 0 ? '+' . $stats['mobile_change'] : $stats['mobile_change'];
     85                                echo esc_html( $change_text );
     86                            ?>% <?php esc_html_e( 'this week', 'opti-behavior' ); ?></div>
    7887                        </div>
    7988                    </div>
     
    8493                            <div class="stat-number"><?php echo esc_html( $stats['avg_time'] ); ?>s</div>
    8594                            <div class="stat-label"><?php esc_html_e( 'Avg. Time on Page', 'opti-behavior' ); ?></div>
    86                             <div class="stat-change positive">+<?php echo esc_html( $stats['time_improvement'] ); ?>% <?php esc_html_e( 'improvement', 'opti-behavior' ); ?></div>
     95                            <div class="stat-change <?php echo $stats['time_improvement'] >= 0 ? 'positive' : 'negative'; ?>"><?php
     96                                $change_text = $stats['time_improvement'] > 0 ? '+' . $stats['time_improvement'] : $stats['time_improvement'];
     97                                echo esc_html( $change_text );
     98                            ?>% <?php esc_html_e( 'this week', 'opti-behavior' ); ?></div>
    8799                        </div>
    88100                    </div>
     
    102114                            <div class="stat-number"><?php echo esc_html( $stats['conversion_rate'] ); ?>%</div>
    103115                            <div class="stat-label"><?php esc_html_e( 'Click-through Rate', 'opti-behavior' ); ?></div>
    104                             <div class="stat-change positive">+<?php echo esc_html( $stats['ctr_improvement'] ); ?>% <?php esc_html_e( 'this month', 'opti-behavior' ); ?></div>
     116                            <div class="stat-change <?php echo $stats['ctr_improvement'] >= 0 ? 'positive' : 'negative'; ?>"><?php
     117                                $change_text = $stats['ctr_improvement'] > 0 ? '+' . $stats['ctr_improvement'] : $stats['ctr_improvement'];
     118                                echo esc_html( $change_text );
     119                            ?>% <?php esc_html_e( 'this week', 'opti-behavior' ); ?></div>
    105120                        </div>
    106121                    </div>
  • opti-behavior/trunk/includes/trait-opti-behavior-sessions-views.php

    r3399182 r3401441  
    216216            <?php
    217217        }
     218
     219        /**
     220         * Get sample sessions data for display.
     221         *
     222         * @since 1.0.4
     223         * @return array Sample sessions data.
     224         */
     225        private function get_sample_sessions_data() {
     226            return array(
     227                array(
     228                    'id'              => '6e7d6c5b4a39',
     229                    'date'            => gmdate( 'Y-m-d H:i:s' ),
     230                    'duration'        => '02:34',
     231                    'alarming_count'  => '3',
     232                    'pages_count'     => '5',
     233                    'pages'           => array( '/home', '/products', '/cart', '/checkout', '/thank-you' ),
     234                    'visitor_type'    => __( 'New Visitor', 'opti-behavior' ),
     235                    'country'         => __( 'United States', 'opti-behavior' ),
     236                    'flag'            => '🇺🇸',
     237                    'referrer'        => 'Google',
     238                ),
     239                array(
     240                    'id'              => '5f6e5d4c3b28',
     241                    'date'            => gmdate( 'Y-m-d H:i:s', strtotime( '-1 hour' ) ),
     242                    'duration'        => '01:45',
     243                    'alarming_count'  => '1',
     244                    'pages_count'     => '3',
     245                    'pages'           => array( '/home', '/about', '/contact' ),
     246                    'visitor_type'    => __( 'Returning', 'opti-behavior' ),
     247                    'country'         => __( 'Canada', 'opti-behavior' ),
     248                    'flag'            => '🇨🇦',
     249                    'referrer'        => __( 'Direct', 'opti-behavior' ),
     250                ),
     251            );
     252        }
    218253    }
    219254}
  • opti-behavior/trunk/readme.txt

    r3399182 r3401441  
    11=== Opti-Behavior ===
    22Contributors: optiuser
    3 Tags: analytics, heatmap, user tracking, session recording, behavior analytics
     3Donate link: https://optiuser.com/
     4Tags: analytics, heatmap, click tracking, user behavior,user insights
     5
    46Requires at least: 5.8
    57Tested up to: 6.8
    68Requires PHP: 7.4
    7 Stable tag: 1.0.4
     9Stable tag: 1.0.5
    810License: GPLv2 or later
    911License URI: https://www.gnu.org/licenses/gpl-2.0.html
    1012
    11 Transform your WordPress site with powerful analytics! Track user behavior with beautiful heatmaps, session recordings, and real-time insights.
     13Powerful website analytics with visual heatmaps, click tracking, and real-time visitor insights. Optimize conversions and improve user experience with data-driven decisions.
    1214
    1315== Description ==
    1416
    15 Opti-Behavior is a comprehensive user behavior analytics plugin for WordPress that helps you understand how visitors interact with your website. Make data-driven decisions to improve user experience and boost conversions.
    16 
    17 = Key Features =
    18 
    19 * **Visual Heatmaps** - See where users click, move, and scroll on your pages with separate mobile and desktop views
    20 * **Session Recordings** - Watch real user sessions to understand behavior patterns
    21 * **Real-time Analytics** - Monitor visitor activity as it happens
    22 * **Page Performance Tracking** - Identify high-performing and underperforming pages
    23 * **User Journey Analysis** - Track visitor paths through your site
    24 * **Bot Detection & Tracking** - Automatic server-side bot detection to filter out non-human traffic
    25 * **File Storage System** - Optional file-based storage for high-traffic sites to prevent database bloat
    26 * **Performance Optimizer** - Automatic database indexing and optimization for large datasets
    27 * **Mobile/Desktop Separation** - Separate heatmap tracking and visualization for mobile and desktop devices
    28 * **Privacy-Focused** - GDPR compliant with built-in data protection features
    29 * **Lightweight & Fast** - Optimized code that won't slow down your site
    30 * **Easy to Use** - Intuitive dashboard with beautiful visualizations
     17**Opti-Behavior** is the ultimate WordPress analytics plugin that reveals exactly how visitors interact with your website. Understand user behavior through powerful visual heatmaps, detailed click tracking, and comprehensive analytics - all stored securely on your own server.
     18
     19Unlike external analytics services, Opti-Behavior keeps all your data local, ensuring complete privacy compliance and lightning-fast performance. Perfect for marketers, designers, and business owners who want to optimize their websites based on real user behavior data.
     20
     21= Why Choose Opti-Behavior? =
     22
     23✓ **100% Privacy-Friendly** - All data stays on your WordPress server (GDPR compliant)
     24✓ **No External Dependencies** - No API calls to third-party services for analytics
     25✓ **Beautiful Visualizations** - Easy-to-understand heatmaps and dashboards
     26✓ **Mobile & Desktop Separation** - Analyze each platform independently
     27✓ **High Performance** - Optimized for sites with millions of pageviews
     28✓ **Bot Filtering** - Automatic detection and exclusion of bot traffic
     29
     30= Core Features (Free Version) =
     31
     32* **Click Heatmaps** - Visualize where users click on your pages with color-coded heatmaps
     33* **Mobile & Desktop Views** - Separate tracking and visualization for different devices
     34* **Real-time Dashboard** - Monitor active visitors and their behavior live
     35* **Page Analytics** - Track pageviews, average time on page, and engagement metrics
     36* **Session Tracking** - Follow user journeys across your website
     37* **Bot Detection** - Server-side detection of search engines and bot traffic
     38* **Performance Optimized** - File-based storage option for high-traffic sites
     39* **Privacy Controls** - IP anonymization, data retention settings, and GDPR compliance tools
     40* **Beautiful UI** - Modern, intuitive interface with dark mode support
     41* **Export Data** - Download your analytics in CSV format
     42
     43= Pro Features (Upgrade) =
     44
     45* **Session Recordings** - Watch real user sessions to see exactly how visitors use your site
     46* **Advanced Heatmaps** - Attention heatmaps, scroll maps, and breakaway analysis
     47* **Unlimited Pages** - Track as many pages as you need (free limited to 5 pages)
     48* **Extended Retention** - Keep data for up to 2 years (free limited to 7 days)
     49* **Priority Support** - Get help when you need it
     50
     51[Learn more about Opti-Behavior Pro](https://optiuser.com/)
    3152
    3253= Perfect For =
    3354
    34 * E-commerce sites looking to optimize conversion funnels
    35 * Content creators wanting to understand reader engagement
    36 * Web designers testing new layouts and designs
    37 * Marketing teams analyzing campaign effectiveness
    38 * Anyone who wants to improve their website's user experience
     55* **E-commerce Stores** - Optimize checkout flows and product pages to increase sales
     56* **Content Publishers** - Understand which content keeps readers engaged
     57* **Web Agencies** - Provide clients with detailed user behavior reports
     58* **SaaS Companies** - Improve onboarding and reduce churn with behavior insights
     59* **Marketing Teams** - Test landing pages and measure campaign effectiveness
     60* **UX Designers** - Validate design decisions with real user data
     61* **Small Businesses** - Make informed decisions without expensive analytics tools
     62
     63= How It Works =
     64
     651. **Install & Activate** - Simple one-click installation, no configuration needed
     662. **Automatic Tracking** - Starts collecting data immediately after activation
     673. **View Insights** - Access beautiful dashboards from your WordPress admin
     684. **Optimize** - Use data-driven insights to improve your website
     695. **Export & Share** - Download reports or share insights with your team
    3970
    4071= Privacy & Data Protection =
    4172
    42 Opti-Behavior takes privacy seriously:
    43 
    44 * GDPR compliant data collection
    45 * Configurable data retention periods
    46 * Option to anonymize IP addresses
    47 * Respect for Do Not Track headers
    48 * User consent management
    49 * Secure data storage
    50 * Complete data deletion on uninstall (optional)
    51 * All data stored locally in your WordPress database
    52 * No external tracking or data sharing with third parties
     73Privacy is built into Opti-Behavior from the ground up:
     74
     75* **GDPR Compliant** - Full compliance with EU privacy regulations
     76* **Local Data Storage** - All analytics data stays in your WordPress database
     77* **IP Anonymization** - Option to anonymize visitor IP addresses
     78* **Data Retention Control** - Set automatic cleanup periods for old data
     79* **No Third-Party Tracking** - Analytics JavaScript is served from your domain
     80* **Consent Management** - Easy integration with consent management plugins
     81* **Data Export** - Users can request their data in portable format
     82* **Complete Deletion** - Option to delete all data on plugin uninstall
     83* **No Cookies Required** - Works without setting cookies (session-based tracking)
     84* **Transparent** - Open-source code you can audit yourself
    5385
    5486= Technical Features =
    5587
    56 * Clean, well-documented code
    57 * WordPress coding standards compliant
    58 * Secure database queries with prepared statements
    59 * Proper data sanitization and escaping
    60 * Efficient database schema with automatic indexing
    61 * Automated data cleanup and retention management
    62 * Flexible storage options (database or file-based)
    63 * Batch processing for high-traffic optimization
    64 * Debug mode with comprehensive logging
    65 * Bot filtering to exclude non-human traffic
    66 * Multisite compatible
    67 * Performance optimizations for large datasets
     88Built with performance and security in mind:
     89
     90* **WordPress Coding Standards** - Fully compliant with WP best practices
     91* **Secure by Design** - All database queries use prepared statements
     92* **Optimized Performance** - Handles millions of events without slowing down
     93* **Flexible Storage** - Choose between database or file-based storage
     94* **Smart Indexing** - Automatic database optimization for fast queries
     95* **Bot Detection** - Server-side filtering of crawlers and bots
     96* **Multisite Ready** - Works perfectly on WordPress multisite networks
     97* **REST API Ready** - Modern architecture with hooks and filters
     98* **Developer Friendly** - Extensive documentation and clean codebase
     99* **Minimal Footprint** - Lightweight JavaScript (~15KB minified)
     100* **Async Loading** - Non-blocking script loading for better page speed
     101* **Batch Processing** - Efficient handling of high-traffic scenarios
    68102
    69103== Installation ==
     
    86120= After Activation =
    87121
    88 1. Navigate to Opti-Behavior in your WordPress admin menu
    89 2. Configure your tracking preferences in Settings
    90 3. Start collecting data immediately
    91 4. View analytics in the Dashboard
     1221. Navigate to **Opti-Behavior** in your WordPress admin menu
     1232. The plugin starts tracking automatically - no configuration required!
     1243. Visit the **Dashboard** to see real-time visitor activity
     1254. Go to **Heatmaps** to view click patterns on your pages
     1265. Adjust **Settings** to customize tracking behavior and privacy options
     127
     128That's it! Opti-Behavior works out of the box with smart defaults.
    92129
    93130== Frequently Asked Questions ==
     
    95132= Does this plugin slow down my website? =
    96133
    97 No! Opti-Behavior is designed to be lightweight and efficient. The tracking script is minified and loads asynchronously, ensuring minimal impact on page load times.
     134Not at all! Opti-Behavior is built for performance. The tracking script is only ~15KB minified and loads asynchronously, so it won't block your page rendering. We've tested it on sites with millions of pageviews without any performance issues.
    98135
    99136= Is this plugin GDPR compliant? =
    100137
    101 Yes! Opti-Behavior includes features for GDPR compliance including IP anonymization, data retention controls, and user consent management. All data is stored locally in your WordPress database and is never sent to external servers. However, you are responsible for configuring these settings appropriately for your use case and ensuring your privacy policy discloses the data collection.
     138Yes! Opti-Behavior is designed with privacy in mind. It includes IP anonymization, data retention controls, and consent management features. All data is stored locally on your server, not sent to third parties. However, you should still configure the settings appropriately for your use case and update your privacy policy to disclose data collection practices.
    102139
    103140= Can I export my data? =
     
    137174= How do I get support? =
    138175
    139 For support, please use the WordPress.org support forums for this plugin. We monitor the forums regularly and will respond as quickly as possible.
     176For support questions, please use the [WordPress.org support forums](https://wordpress.org/support/plugin/opti-behavior/). For premium support, consider upgrading to [Opti-Behavior Pro](https://optiuser.com/).
     177
     178= How is this different from Google Analytics? =
     179
     180Unlike Google Analytics, Opti-Behavior stores all data on YOUR server (not Google's), provides visual heatmaps to see exactly where users click, doesn't require cookie consent in most cases, and gives you session-level insights. Plus, it's specifically designed for WordPress and integrates seamlessly with your admin dashboard.
     181
     182= Can I use this with WooCommerce? =
     183
     184Absolutely! Opti-Behavior works great with WooCommerce. Use heatmaps to optimize product pages, track cart abandonment patterns, and analyze checkout flow behavior to increase conversions.
     185
     186= What's the difference between Free and Pro? =
     187
     188The free version includes click heatmaps, real-time analytics, and tracks up to 5 pages with 7-day data retention. Pro unlocks session recordings, advanced heatmaps (attention, scroll, breakaway), unlimited pages, 2-year retention, and priority support. [Compare plans](https://optiuser.com/)
     189
     190= Does it work with page builders? =
     191
     192Yes! Opti-Behavior works with all major page builders including Elementor, Divi, Beaver Builder, WPBakery, and Gutenberg.
    140193
    141194= What is the File Storage System? =
     
    153206== External Services ==
    154207
    155 This plugin connects to external services in specific circumstances:
    156 
    157 = IP Geolocation API (ip-api.com) =
    158 
    159 **What it is:** A free IP geolocation service that provides geographic information based on IP addresses.
    160 
    161 **When it's used:** When a visitor accesses your site and their location cannot be determined from CloudFlare headers (if CloudFlare is not in use), the plugin may make a server-side API call to ip-api.com to determine the visitor's approximate geographic location (country, region, city, timezone) based on their IP address.
    162 
    163 **What data is sent:** Only the visitor's IP address is sent to ip-api.com. No other personally identifiable information is transmitted.
    164 
    165 **Why it's needed:** This geolocation data is used to provide geographic analytics in the dashboard (e.g., visitor location maps, country/region statistics).
     208This plugin may connect to external services in limited circumstances:
     209
     210= IP Geolocation (ip-api.com) =
     211
     212**Purpose:** Provides geographic location data (country, city, timezone) for visitor analytics and map visualization.
     213
     214**When Used:** Only when a visitor's location cannot be determined from CloudFlare headers. If CloudFlare is active, no external calls are made.
     215
     216**Data Sent:** Only the visitor's IP address. No personally identifiable information is transmitted.
    166217
    167218**Caching:** Results are cached for 1 hour to minimize API requests.
    168219
    169 **Rate limits:** The free tier has a rate limit of 45 requests per minute.
    170 
    171 **Privacy & Terms:**
    172 * Service homepage: https://ip-api.com/
    173 * Terms of service: https://ip-api.com/docs/legal
    174 * Privacy policy: https://ip-api.com/docs/legal (see Privacy section)
    175 * Data handling: ip-api.com may log IP addresses for their own analytics. Please review their privacy policy for details.
    176 
    177 **Note:** If CloudFlare is detected, the plugin uses CloudFlare's provided geolocation headers instead and does NOT make external API calls.
     220**Privacy:**
     221* Service: https://ip-api.com/
     222* Terms & Privacy: https://ip-api.com/docs/legal
     223* Note: ip-api.com may log IP addresses. Review their privacy policy for details.
    178224
    179225= OpenStreetMap Tiles =
    180226
    181 **What it is:** Map tiles for displaying the real-time visitor map visualization.
    182 
    183 **When it's used:** When you view the real-time visitor map in the admin dashboard.
    184 
    185 **What data is sent:** Map tile requests are made directly by your browser (not by the plugin server) to OpenStreetMap tile servers. Standard HTTP request data (IP address, user agent) is sent as part of normal browser requests.
    186 
    187 **Why it's needed:** To display the interactive map showing visitor locations.
    188 
    189 **Privacy & Terms:**
    190 * Service homepage: https://www.openstreetmap.org/
    191 * Tile usage policy: https://operations.osmfoundation.org/policies/tiles/
    192 * Privacy policy: https://wiki.osmfoundation.org/wiki/Privacy_Policy
     227**Purpose:** Displays the interactive visitor location map in your WordPress admin dashboard.
     228**When Used:** Only when YOU (the admin) view the real-time visitor map. Not used on the frontend.
     229**Data Sent:** Your browser makes direct requests to OpenStreetMap tile servers (standard HTTP headers only).
     230
     231**Privacy:**
     232* Service: https://www.openstreetmap.org/
     233* Tile Policy: https://operations.osmfoundation.org/policies/tiles/
     234* Privacy Policy: https://wiki.osmfoundation.org/wiki/Privacy_Policy
     235
     236**Important Note:** All analytics data is stored locally on your WordPress server. The services above are only used for optional geographic features and map visualization.
    193237
    194238== Screenshots ==
     
    203247== Changelog ==
    204248
    205 = 1.0.4 =
     249= 1.0.5 - 2025-01-23 =
     250* Fix: Removed all debug error_log() calls from production code
     251* Fix: Replaced date() with gmdate() for timezone-safe date handling
     252* Fix: Added translator comments for i18n compliance
     253* Fix: Updated API URL from localhost to production endpoint
     254* Fix: Corrected stable tag version mismatch
     255* Enhancement: Improved readme with better descriptions and FAQ
     256* Enhancement: Added Plugin URI and updated Author URI
     257* Enhancement: Optimized WordPress.org directory submission compliance
     258* Compatibility: Full WordPress 6.8 compatibility verified
     259* Enhancement: Added COALESCE for better handling of NULL titles in Top Pages
     260* Enhancement: Improved country detection with browser language fallback when IP geolocation fails
     261* Enhancement: Top Pages widget now displays page views instead of clicks for better accuracy
     262
     263
     264= 1.0.4 - 2025-01-20 =
    206265* Enhancement: Added file-based storage system for high-traffic sites
    207266* Enhancement: Implemented automatic bot detection and filtering
     
    209268* Enhancement: Separate mobile and desktop heatmap tracking and visualization
    210269* Enhancement: Batch processing for improved performance
    211 * Enhancement: Enhanced debug logging system
     270* Enhancement: Enhanced debug logging system with WordPress-compliant manager
    212271* Enhancement: Added Lucide icon library (v0.554.0, ISC License) for modern UI
    213 * Security: Replaced error_log() with WordPress-compliant debug manager
    214272* Security: Replaced direct filesystem operations with WP_Filesystem API
    215273* Security: Replaced unlink() with wp_delete_file() for file deletion
     
    251309== Upgrade Notice ==
    252310
     311= 1.0.4.19 =
     312WordPress.org submission update: Fixes debug code, timezone handling, and i18n compliance. All users should update for WordPress coding standards compliance.
     313
    253314= 1.0.4 =
    254 Major update with file storage system, bot detection, performance optimizations, and WordPress coding standards compliance. Recommended for all users, especially high-traffic sites.
     315Major update with file storage system, bot detection, and performance optimizations. Recommended for all users, especially high-traffic sites.
    255316
    256317= 1.0.3 =
    257 Important security update with enhanced SQL security and WordPress coding standards compliance. Recommended for all users.
    258 
    259 = 1.0.1 =
    260 Important security update with enhanced input sanitization and WordPress coding standards compliance. Recommended for all users.
     318Important security update with enhanced SQL security. All users should update.
    261319
    262320= 1.0.0 =
    263 Initial release of Opti-Behavior. Install to start tracking user behavior on your WordPress site.
     321Initial release. Install to start tracking user behavior with powerful heatmaps and analytics.
    264322
    265323== Privacy Policy ==
Note: See TracChangeset for help on using the changeset viewer.