Plugin Directory

Changeset 3488602


Ignore:
Timestamp:
03/23/2026 06:34:16 AM (10 days ago)
Author:
bsolveit
Message:

v1.3.4: Chart.js visualisations, Rebuild Summary button, CDN-compatible logging

  • Replace CSS bar charts with Chart.js for polished, responsive visualisations
  • Stacked bar chart with animated transitions and index tooltips
  • Bot detail trend chart rendered with Chart.js
  • Rebuild Summary button for immediate dashboard updates
  • Chart shows full date range with proper bar sizing
  • Bot detection in serve_file() for CDN compatibility
  • s-maxage=0 on discovery file responses
  • Analytics scoped to discovery file access only
  • Single dashboard widget (Discovery File Access)
Location:
ai-discovery-files
Files:
58 added
8 edited

Legend:

Unmodified
Added
Removed
  • ai-discovery-files/trunk/admin/class-admin.php

    r3488558 r3488602  
    4444        add_action( 'wp_ajax_aidf_crawler_export_csv', array( __CLASS__, 'ajax_crawler_export_csv' ) );
    4545        add_action( 'wp_ajax_aidf_crawler_clear_log', array( __CLASS__, 'ajax_crawler_clear_log' ) );
     46        add_action( 'wp_ajax_aidf_crawler_rebuild_summary', array( __CLASS__, 'ajax_crawler_rebuild_summary' ) );
    4647        add_action( 'wp_ajax_aidf_crawler_conflicts', array( __CLASS__, 'ajax_crawler_conflicts' ) );
    4748        add_action( 'wp_ajax_aidf_crawler_file_access', array( __CLASS__, 'ajax_crawler_file_access' ) );
     
    152153        if ( 'crawlers' === $current_tab ) {
    153154            wp_enqueue_style( 'aidf-crawlers', AIDF_PLUGIN_URL . 'admin/css/crawlers.css', array( 'aidf-admin' ), AIDF_VERSION );
    154             wp_enqueue_script( 'aidf-crawlers', AIDF_PLUGIN_URL . 'admin/js/crawlers.js', array( 'jquery' ), AIDF_VERSION, true );
     155            wp_enqueue_script( 'aidf-chartjs', AIDF_PLUGIN_URL . 'admin/js/chart.min.js', array(), '4.4.8', true );
     156            wp_enqueue_script( 'aidf-crawlers', AIDF_PLUGIN_URL . 'admin/js/crawlers.js', array( 'jquery', 'aidf-chartjs' ), AIDF_VERSION, true );
    155157        }
    156158    }
     
    881883
    882884    /**
     885     * AJAX: Rebuild the crawler summary table from the raw log.
     886     *
     887     * Triggers an immediate summary rebuild so the dashboard reflects the
     888     * latest data without waiting for the daily cron job.
     889     *
     890     * @since 1.3.2
     891     */
     892    public static function ajax_crawler_rebuild_summary() {
     893        check_ajax_referer( 'aidf_admin_nonce', 'nonce' );
     894        if ( ! current_user_can( 'manage_options' ) ) {
     895            wp_send_json_error( __( 'Permission denied.', 'ai-discovery-files' ) );
     896        }
     897
     898        AIDF_Crawler_Analytics::rebuild_summary();
     899
     900        wp_send_json_success( array(
     901            'message' => __( 'Summary rebuilt successfully.', 'ai-discovery-files' ),
     902        ) );
     903    }
     904
     905    /**
    883906     * AJAX: Crawler conflicts data.
    884907     *
  • ai-discovery-files/trunk/admin/css/crawlers.css

    r3488445 r3488602  
    130130
    131131/* ==========================================================
    132    4. Bar Chart
    133    ========================================================== */
    134 
    135 .aidf-bar-chart {
    136     background: var(--aidf-panel);
    137     border: 1px solid var(--aidf-border);
    138     border-radius: var(--aidf-radius-md);
    139     padding: var(--aidf-sp-6);
    140     box-shadow: var(--aidf-shadow-sm);
    141     min-height: 240px;
     132   4. Chart.js Canvas
     133   ========================================================== */
     134
     135.aidf-chart-canvas-wrap {
    142136    position: relative;
    143 }
    144 
    145 .aidf-bar-chart__grid {
    146     display: grid;
    147     grid-auto-columns: 1fr;
    148     grid-auto-flow: column;
    149     gap: var(--aidf-sp-2);
    150     align-items: end;
    151     height: 200px;
    152     padding-bottom: var(--aidf-sp-6);
    153 }
    154 
    155 .aidf-bar-chart__bar {
    156     display: flex;
    157     flex-direction: column-reverse;
    158     align-items: stretch;
    159     height: 100%;
    160     position: relative;
    161     gap: 1px;
    162 }
    163 
    164 .aidf-bar-chart__segment {
    165     border-radius: var(--aidf-radius-sm) var(--aidf-radius-sm) 0 0;
    166     min-height: 2px;
    167     transition: height var(--aidf-transition-slow);
    168     cursor: pointer;
    169 }
    170 
    171 .aidf-bar-chart__segment:first-child {
    172     border-radius: 0 0 var(--aidf-radius-sm) var(--aidf-radius-sm);
    173 }
    174 
    175 .aidf-bar-chart__segment:only-child {
    176     border-radius: var(--aidf-radius-sm);
    177 }
    178 
    179 .aidf-bar-chart__label {
    180     position: absolute;
    181     bottom: calc(-1 * var(--aidf-sp-6));
    182     left: 50%;
    183     transform: translateX(-50%);
    184     font-size: var(--aidf-text-xs);
     137    height: 250px;
     138    width: 100%;
     139    padding: var(--aidf-sp-4) var(--aidf-sp-6) var(--aidf-sp-6);
     140}
     141
     142.aidf-chart-canvas-wrap--detail {
     143    height: 180px;
     144    padding: 0;
     145}
     146
     147.aidf-chart-empty {
     148    text-align: center;
     149    padding: var(--aidf-sp-8) var(--aidf-sp-4);
    185150    color: var(--aidf-text-tertiary);
    186     white-space: nowrap;
    187 }
    188 
    189 .aidf-bar-chart__tooltip {
    190     position: absolute;
    191     bottom: calc(100% + var(--aidf-sp-2));
    192     left: 50%;
    193     transform: translateX(-50%);
    194     background: var(--aidf-dark);
    195     color: var(--aidf-text-inverse);
    196     padding: var(--aidf-sp-2) var(--aidf-sp-3);
    197     border-radius: var(--aidf-radius);
    198     font-size: var(--aidf-text-xs);
    199     white-space: nowrap;
    200     z-index: 10;
    201     box-shadow: var(--aidf-shadow-md);
    202     pointer-events: none;
    203     opacity: 0;
    204     transition: opacity var(--aidf-transition);
    205 }
    206 
    207 .aidf-bar-chart__bar:hover .aidf-bar-chart__tooltip {
    208     opacity: 1;
    209 }
    210 
    211 .aidf-bar-chart__tooltip::after {
    212     content: '';
    213     position: absolute;
    214     top: 100%;
    215     left: 50%;
    216     transform: translateX(-50%);
    217     border: 5px solid transparent;
    218     border-top-color: var(--aidf-dark);
     151    font-size: var(--aidf-text-sm);
     152    margin: 0;
    219153}
    220154
     
    1018952
    1019953/* ==========================================================
    1020    22. Chart Enhancements (inside dashboard panel)
    1021    ========================================================== */
    1022 
    1023 .aidf-dashboard-panel .aidf-bar-chart {
    1024     display: flex;
    1025     gap: var(--aidf-sp-4);
    1026     padding: var(--aidf-sp-4) var(--aidf-sp-6) var(--aidf-sp-6);
    1027     background: transparent;
    1028     border: none;
    1029     box-shadow: none;
    1030     border-radius: 0;
    1031     min-height: auto;
    1032 }
    1033 
    1034 .aidf-bar-chart__y-axis {
    1035     display: flex;
    1036     flex-direction: column;
    1037     justify-content: space-between;
    1038     align-items: flex-end;
    1039     width: 40px;
    1040     flex-shrink: 0;
    1041     padding-bottom: var(--aidf-sp-6);
    1042 }
    1043 
    1044 .aidf-bar-chart__y-label {
    1045     font-size: var(--aidf-text-xs);
    1046     color: var(--aidf-text-tertiary);
    1047     line-height: 1;
    1048     font-variant-numeric: tabular-nums;
    1049 }
    1050 
    1051 .aidf-bar-chart__empty {
    1052     display: flex;
    1053     align-items: center;
    1054     justify-content: center;
    1055     width: 100%;
    1056     height: 200px;
    1057     font-size: var(--aidf-text-sm);
    1058     color: var(--aidf-text-tertiary);
    1059 }
     954   22. (Reserved — previously CSS bar chart enhancements)
     955   ========================================================== */
    1060956
    1061957/* ==========================================================
     
    12691165    }
    12701166
    1271     .aidf-bar-chart__grid {
    1272         gap: 1px;
    1273     }
    1274 
    1275     .aidf-bar-chart__y-axis {
    1276         display: none;
    1277     }
    1278 
    12791167    .aidf-log-filters {
    12801168        flex-direction: column;
     
    13891277}
    13901278
    1391 /* Trend chart (single-colour bars) */
    1392 .aidf-detail-trend {
    1393     display: flex;
    1394     align-items: flex-end;
    1395     gap: 3px;
    1396     height: 120px;
    1397     padding-bottom: var(--aidf-sp-4); /* room for labels */
    1398     position: relative;
    1399 }
    1400 
    1401 .aidf-detail-trend__bar {
    1402     flex: 1;
    1403     display: flex;
    1404     flex-direction: column;
    1405     align-items: center;
    1406     justify-content: flex-end;
    1407     height: 100%;
    1408     cursor: default;
    1409 }
    1410 
    1411 .aidf-detail-trend__fill {
    1412     width: 100%;
    1413     background: var(--aidf-orange);
    1414     border-radius: var(--aidf-radius-sm) var(--aidf-radius-sm) 0 0;
    1415     min-height: 2px;
    1416     transition: opacity 0.15s;
    1417 }
    1418 
    1419 .aidf-detail-trend__bar:hover .aidf-detail-trend__fill {
    1420     opacity: 0.75;
    1421 }
    1422 
    1423 .aidf-detail-trend__label {
    1424     font-size: 10px;
    1425     color: var(--aidf-text-tertiary);
    1426     margin-top: var(--aidf-sp-1);
    1427     white-space: nowrap;
    1428     overflow: hidden;
    1429     text-overflow: ellipsis;
    1430     max-width: 100%;
    1431 }
     1279/* Trend chart — now rendered via Chart.js canvas (see .aidf-chart-canvas-wrap--detail) */
    14321280
    14331281/* Status code horizontal stacked bar */
  • ai-discovery-files/trunk/admin/js/crawlers.js

    r3488558 r3488602  
    6969     */
    7070    var botColors = [
    71         'var(--aidf-orange)',
    72         'var(--aidf-blue)',
    73         'var(--aidf-green)',
    74         'var(--aidf-amber)',
    75         'var(--aidf-red)',
     71        '#e77d15',
     72        '#2563eb',
     73        '#16a34a',
     74        '#d97706',
     75        '#dc2626',
    7676        '#8b5cf6'
    7777    ];
    78     var otherColor = 'var(--aidf-gray-border)';
     78    var otherColor = '#e5e7eb';
    7979
    8080    /* ==========================================================
     
    8484    var currentDays = parseInt($('#aidf-crawlers-dashboard').data('days'), 10) || 30;
    8585    var conflictsData = null; // Loaded once on init.
     86    var activityChart = null;    // Chart.js instance for main activity chart.
     87    var detailTrendChart = null; // Chart.js instance for bot detail trend chart.
    8688
    8789    /* ==========================================================
     
    135137
    136138    (function initChart() {
    137         var $grid = $('#aidf-chart-grid');
    138         if (!$grid.length) {
     139        var $canvas = $('#aidf-activity-chart');
     140        if (!$canvas.length) {
    139141            return;
    140142        }
     
    260262
    261263    function renderChart(chartData, days) {
    262         var $grid   = $('#aidf-chart-grid');
    263         var $yAxis  = $('#aidf-chart-y-axis');
     264        var $canvas = $('#aidf-activity-chart');
    264265        var $legend = $('#aidf-chart-legend');
    265266        var $tbody  = $('#aidf-chart-data-tbody');
    266 
    267         if (!$grid.length) {
    268             return;
     267        var $empty  = $('#aidf-chart-empty');
     268        var $wrap   = $canvas.closest('.aidf-chart-canvas-wrap');
     269
     270        if (!$canvas.length) {
     271            return;
     272        }
     273
     274        // Destroy previous Chart.js instance.
     275        if (activityChart) {
     276            activityChart.destroy();
     277            activityChart = null;
    269278        }
    270279
     
    272281        var buckets = Object.keys(chartData);
    273282        if (!buckets.length) {
    274             $grid.html('<div class="aidf-bar-chart__empty">' + escHtml('No activity in this period') + '</div>');
    275             $yAxis.empty();
     283            $wrap.hide();
     284            $empty.show();
    276285            $legend.empty();
    277286            $tbody.empty();
     
    279288        }
    280289
     290        // Show canvas, hide empty message.
     291        $wrap.show();
     292        $empty.hide();
     293
    281294        // Determine all bot names across all buckets and find the top 6.
    282295        var botTotals = {};
    283         var i, j, bucket, bots, botName;
     296        var i, j, bots, botName;
    284297
    285298        for (i = 0; i < buckets.length; i++) {
     
    313326        }
    314327
    315         // Calculate max bar total.
    316         var maxTotal = 0;
     328        // Build labels (date buckets formatted for display).
     329        var labels = [];
    317330        for (i = 0; i < buckets.length; i++) {
    318             var barTotal = 0;
     331            var label = buckets[i];
     332            if (days <= 30 && label.length === 10) {
     333                var parts = label.split('-');
     334                label = parseInt(parts[1], 10) + '/' + parseInt(parts[2], 10);
     335            }
     336            labels.push(label);
     337        }
     338
     339        // Build datasets — one per bot in legend order.
     340        var datasets = [];
     341        var allBots = legendBots.slice(); // Copy so we include all mapped bots.
     342
     343        // Also include bots beyond top 6 that are not 'Other' — they go into Other's data.
     344        // Actually, the server already groups beyond top N into 'Other'.
     345        // Build one dataset per legendBot.
     346        for (i = 0; i < allBots.length; i++) {
     347            botName = allBots[i];
     348            var data = [];
     349            for (j = 0; j < buckets.length; j++) {
     350                data.push(chartData[buckets[j]][botName] || 0);
     351            }
     352            datasets.push({
     353                label: botName,
     354                data: data,
     355                backgroundColor: botName === 'Other' ? otherColor : (colorMap[botName] || otherColor),
     356                borderRadius: 3,
     357                borderSkipped: false
     358            });
     359        }
     360
     361        // Populate screen-reader data table.
     362        var srHtml = '';
     363        for (i = 0; i < buckets.length; i++) {
    319364            bots = chartData[buckets[i]];
    320365            for (botName in bots) {
    321                 if (bots.hasOwnProperty(botName)) {
    322                     barTotal += bots[botName];
     366                if (bots.hasOwnProperty(botName) && bots[botName] > 0) {
     367                    srHtml += '<tr><td>' + escHtml(buckets[i]) + '</td><td>' + escHtml(botName) + '</td><td>' + formatNumber(bots[botName]) + '</td></tr>';
    323368                }
    324369            }
    325             if (barTotal > maxTotal) {
    326                 maxTotal = barTotal;
    327             }
    328         }
    329         if (maxTotal < 1) {
    330             maxTotal = 1;
    331         }
    332 
    333         // Render Y-axis labels.
    334         var yLabels = [0, Math.round(maxTotal * 0.25), Math.round(maxTotal * 0.5), Math.round(maxTotal * 0.75), maxTotal];
    335         var yHtml = '';
    336         for (i = yLabels.length - 1; i >= 0; i--) {
    337             yHtml += '<span class="aidf-bar-chart__y-label">' + formatNumber(yLabels[i]) + '</span>';
    338         }
    339         $yAxis.html(yHtml);
    340 
    341         // Render bars.
    342         var gridHtml = '';
    343         var srHtml   = '';
    344 
    345         for (i = 0; i < buckets.length; i++) {
    346             bucket = buckets[i];
    347             bots   = chartData[bucket];
    348 
    349             // Format label.
    350             var label = bucket;
    351             if (days <= 30 && bucket.length === 10) {
    352                 // Format YYYY-MM-DD to shorter display.
    353                 var parts = bucket.split('-');
    354                 label = parseInt(parts[1], 10) + '/' + parseInt(parts[2], 10);
    355             }
    356 
    357             // Build segments (from bottom to top = reverse of sorted order).
    358             var segments = '';
    359             var tooltipLines = [];
    360             var barTotal = 0;
    361 
    362             // Process bots in sorted order.
    363             for (j = 0; j < sortedBots.length; j++) {
    364                 botName = sortedBots[j];
    365                 var count = bots[botName] || 0;
    366                 if (count <= 0) {
    367                     continue;
    368                 }
    369                 barTotal += count;
    370 
    371                 var color = colorMap[botName] || otherColor;
    372                 var pct   = (count / maxTotal) * 100;
    373                 segments += '<div class="aidf-bar-chart__segment" style="height:' + pct.toFixed(1) + '%;background:' + color + ';" title="' + escHtml(botName) + ': ' + formatNumber(count) + '"></div>';
    374                 tooltipLines.push(escHtml(botName) + ': ' + formatNumber(count));
    375 
    376                 // Screen reader data.
    377                 srHtml += '<tr><td>' + escHtml(bucket) + '</td><td>' + escHtml(botName) + '</td><td>' + formatNumber(count) + '</td></tr>';
    378             }
    379 
    380             // Build tooltip.
    381             var tooltip = '<div class="aidf-bar-chart__tooltip"><strong>' + escHtml(label) + '</strong>';
    382             if (tooltipLines.length) {
    383                 tooltip += '<br>' + tooltipLines.join('<br>');
    384             }
    385             tooltip += '<br><strong>Total: ' + formatNumber(barTotal) + '</strong></div>';
    386 
    387             gridHtml += '<div class="aidf-bar-chart__bar">' +
    388                 tooltip + segments +
    389                 '<span class="aidf-bar-chart__label">' + escHtml(label) + '</span>' +
    390             '</div>';
    391         }
    392 
    393         $grid.html(gridHtml);
     370        }
    394371        $tbody.html(srHtml);
    395372
    396         // Render legend.
     373        // Render custom legend.
    397374        var legendHtml = '';
    398375        for (i = 0; i < legendBots.length; i++) {
     
    404381        }
    405382        $legend.html(legendHtml);
     383
     384        // Create the Chart.js stacked bar chart.
     385        activityChart = new Chart($canvas[0].getContext('2d'), {
     386            type: 'bar',
     387            data: {
     388                labels: labels,
     389                datasets: datasets
     390            },
     391            options: {
     392                responsive: true,
     393                maintainAspectRatio: false,
     394                plugins: {
     395                    legend: { display: false },
     396                    tooltip: {
     397                        mode: 'index',
     398                        intersect: false,
     399                        callbacks: {
     400                            label: function(ctx) {
     401                                if (ctx.parsed.y === 0) {
     402                                    return null;
     403                                }
     404                                return ctx.dataset.label + ': ' + ctx.parsed.y.toLocaleString();
     405                            }
     406                        }
     407                    }
     408                },
     409                scales: {
     410                    x: {
     411                        stacked: true,
     412                        grid: { display: false },
     413                        ticks: {
     414                            maxRotation: 0,
     415                            autoSkip: true,
     416                            maxTicksLimit: 10,
     417                            font: { size: 11 }
     418                        }
     419                    },
     420                    y: {
     421                        stacked: true,
     422                        beginAtZero: true,
     423                        grid: { color: 'rgba(0,0,0,0.04)' },
     424                        ticks: {
     425                            precision: 0,
     426                            font: { size: 11 }
     427                        }
     428                    }
     429                },
     430                interaction: {
     431                    mode: 'index',
     432                    intersect: false
     433                }
     434            }
     435        });
    406436    }
    407437
     
    688718            } else {
    689719                showNotice('error', response.data || 'Save failed.');
     720            }
     721        }).fail(function() {
     722            $btn.prop('disabled', false).text(original);
     723            showNotice('error', 'Network error. Please try again.');
     724        });
     725    });
     726
     727    /* ==========================================================
     728       Rebuild Summary
     729       ========================================================== */
     730
     731    $('#aidf-rebuild-summary').on('click', function() {
     732        var $btn     = $(this);
     733        var original = $btn.text();
     734
     735        $btn.prop('disabled', true).text('Rebuilding\u2026');
     736
     737        $.post(aidf.ajaxUrl, {
     738            action: 'aidf_crawler_rebuild_summary',
     739            nonce:  aidf.nonce
     740        }, function(response) {
     741            $btn.prop('disabled', false).text(original);
     742            if (response.success) {
     743                showNotice('success', response.data.message);
     744                // Reload the dashboard to reflect rebuilt data.
     745                if ($('#aidf-summary-cards').length) {
     746                    loadDashboard(currentDays);
     747                }
     748            } else {
     749                showNotice('error', response.data || 'Failed to rebuild summary.');
    690750            }
    691751        }).fail(function() {
     
    824884    $(document).on('click', '#aidf-back-to-overview', function(e) {
    825885        e.preventDefault();
    826         // Clear the bot detail panels before hiding, so stale data isn't shown.
    827         $('#aidf-bot-detail-chart, #aidf-bot-detail-status, #aidf-bot-detail-pages, #aidf-bot-detail-files').empty();
     886        // Destroy the detail trend chart to prevent stale data.
     887        if (detailTrendChart) {
     888            detailTrendChart.destroy();
     889            detailTrendChart = null;
     890        }
     891        // Clear the other detail panels before hiding.
     892        $('#aidf-bot-detail-status, #aidf-bot-detail-pages, #aidf-bot-detail-files').empty();
    828893        hideBotDetail();
    829894    });
     
    858923        // Show loading state inside each panel.
    859924        var loadingHtml = '<div class="aidf-detail-loading"><span class="spinner is-active" style="float:none;"></span></div>';
    860         $('#aidf-bot-detail-chart, #aidf-bot-detail-status, #aidf-bot-detail-pages, #aidf-bot-detail-files').html(loadingHtml);
     925        // Trend chart panel has a persistent canvas; destroy chart and hide canvas during load.
     926        if (detailTrendChart) {
     927            detailTrendChart.destroy();
     928            detailTrendChart = null;
     929        }
     930        $('#aidf-detail-trend-chart').closest('.aidf-chart-canvas-wrap').hide();
     931        $('#aidf-detail-trend-empty').hide();
     932        $('#aidf-bot-detail-status, #aidf-bot-detail-pages, #aidf-bot-detail-files').html(loadingHtml);
    861933        $('#aidf-bot-detail-first-seen, #aidf-bot-detail-last-seen').text('');
    862934
     
    874946            if (!response.success) {
    875947                var errHtml = '<p class="aidf-text-muted">' + escHtml(response.data || 'Failed to load.') + '</p>';
    876                 $('#aidf-bot-detail-chart, #aidf-bot-detail-status, #aidf-bot-detail-pages, #aidf-bot-detail-files').html(errHtml);
     948                $('#aidf-detail-trend-empty').text(response.data || 'Failed to load.').show();
     949                $('#aidf-bot-detail-status, #aidf-bot-detail-pages, #aidf-bot-detail-files').html(errHtml);
    877950                return;
    878951            }
     
    898971        }).fail(function() {
    899972            var errHtml = '<p class="aidf-text-muted">Network error. Please try again.</p>';
    900             $('#aidf-bot-detail-chart, #aidf-bot-detail-status, #aidf-bot-detail-pages, #aidf-bot-detail-files').html(errHtml);
    901         });
    902     }
    903 
    904     /**
    905      * Render a single-colour bar chart for daily access trend.
    906      * Bars are proportional to the day with the highest count.
     973            $('#aidf-detail-trend-empty').text('Network error. Please try again.').show();
     974            $('#aidf-bot-detail-status, #aidf-bot-detail-pages, #aidf-bot-detail-files').html(errHtml);
     975        });
     976    }
     977
     978    /**
     979     * Render a single-colour bar chart for daily access trend using Chart.js.
    907980     */
    908981    function renderDetailTrendChart(dailyTrend) {
    909         var $container = $('#aidf-bot-detail-chart');
     982        var $canvas = $('#aidf-detail-trend-chart');
     983        var $empty  = $('#aidf-detail-trend-empty');
     984        var $wrap   = $canvas.closest('.aidf-chart-canvas-wrap');
     985
     986        // Destroy previous Chart.js instance.
     987        if (detailTrendChart) {
     988            detailTrendChart.destroy();
     989            detailTrendChart = null;
     990        }
    910991
    911992        var dates = Object.keys(dailyTrend).sort();
    912993        if (!dates.length) {
    913             $container.html('<p class="aidf-text-muted">No access data in this period.</p>');
    914             return;
    915         }
    916 
    917         // Find max.
    918         var maxVal = 0;
    919         var i;
    920         for (i = 0; i < dates.length; i++) {
    921             if (dailyTrend[dates[i]] > maxVal) {
    922                 maxVal = dailyTrend[dates[i]];
    923             }
    924         }
    925         if (maxVal < 1) {
    926             maxVal = 1;
    927         }
    928 
    929         var html = '<div class="aidf-detail-trend">';
    930         for (i = 0; i < dates.length; i++) {
    931             var date  = dates[i];
    932             var count = dailyTrend[date] || 0;
    933             var pct   = Math.max(2, Math.round((count / maxVal) * 100));
    934 
    935             // Format date label: strip year, show MM/DD.
    936             var parts = date.split('-');
    937             var label = parts.length === 3 ? parseInt(parts[1], 10) + '/' + parseInt(parts[2], 10) : date;
    938 
    939             html += '<div class="aidf-detail-trend__bar" title="' + escHtml(date) + ': ' + formatNumber(count) + ' accesses">' +
    940                 '<div class="aidf-detail-trend__fill" style="height:' + pct + '%;"></div>' +
    941                 '<span class="aidf-detail-trend__label">' + escHtml(label) + '</span>' +
    942             '</div>';
    943         }
    944         html += '</div>';
    945 
    946         $container.html(html);
     994            $wrap.hide();
     995            $empty.show();
     996            return;
     997        }
     998
     999        $wrap.show();
     1000        $empty.hide();
     1001
     1002        // Build labels and data arrays.
     1003        var labels = [];
     1004        var data   = [];
     1005        for (var i = 0; i < dates.length; i++) {
     1006            var parts = dates[i].split('-');
     1007            labels.push(parts.length === 3 ? parseInt(parts[1], 10) + '/' + parseInt(parts[2], 10) : dates[i]);
     1008            data.push(dailyTrend[dates[i]] || 0);
     1009        }
     1010
     1011        detailTrendChart = new Chart($canvas[0].getContext('2d'), {
     1012            type: 'bar',
     1013            data: {
     1014                labels: labels,
     1015                datasets: [{
     1016                    label: 'Accesses',
     1017                    data: data,
     1018                    backgroundColor: '#e77d15',
     1019                    borderRadius: 3,
     1020                    borderSkipped: false
     1021                }]
     1022            },
     1023            options: {
     1024                responsive: true,
     1025                maintainAspectRatio: false,
     1026                plugins: {
     1027                    legend: { display: false },
     1028                    tooltip: {
     1029                        callbacks: {
     1030                            label: function(ctx) {
     1031                                return ctx.parsed.y.toLocaleString() + ' accesses';
     1032                            }
     1033                        }
     1034                    }
     1035                },
     1036                scales: {
     1037                    x: {
     1038                        grid: { display: false },
     1039                        ticks: {
     1040                            maxRotation: 0,
     1041                            autoSkip: true,
     1042                            maxTicksLimit: 10,
     1043                            font: { size: 11 }
     1044                        }
     1045                    },
     1046                    y: {
     1047                        beginAtZero: true,
     1048                        grid: { color: 'rgba(0,0,0,0.04)' },
     1049                        ticks: {
     1050                            precision: 0,
     1051                            font: { size: 11 }
     1052                        }
     1053                    }
     1054                }
     1055            }
     1056        });
    9471057    }
    9481058
  • ai-discovery-files/trunk/admin/views/partials/crawler-bot-detail.php

    r3488558 r3488602  
    4040            <h4 class="aidf-panel-title"><?php esc_html_e( 'Access Trend', 'ai-discovery-files' ); ?></h4>
    4141        </div>
    42         <div class="aidf-panel-body" id="aidf-bot-detail-chart"></div>
     42        <div class="aidf-panel-body" id="aidf-bot-detail-chart">
     43            <div class="aidf-chart-canvas-wrap aidf-chart-canvas-wrap--detail">
     44                <canvas id="aidf-detail-trend-chart"></canvas>
     45            </div>
     46            <p class="aidf-chart-empty" id="aidf-detail-trend-empty" style="display:none;"><?php esc_html_e( 'No access data in this period.', 'ai-discovery-files' ); ?></p>
     47        </div>
    4348    </div>
    4449
  • ai-discovery-files/trunk/admin/views/tab-crawlers.php

    r3488558 r3488602  
    153153                    <div class="aidf-chart-legend" id="aidf-chart-legend"></div>
    154154                </div>
    155                 <div class="aidf-bar-chart" id="aidf-activity-chart">
    156                     <div class="aidf-bar-chart__y-axis" id="aidf-chart-y-axis"></div>
    157                     <div class="aidf-bar-chart__grid" id="aidf-chart-grid"></div>
    158                 </div>
     155                <div class="aidf-chart-canvas-wrap">
     156                    <canvas id="aidf-activity-chart"></canvas>
     157                </div>
     158                <p class="aidf-chart-empty" id="aidf-chart-empty" style="display:none;"><?php esc_html_e( 'No activity in this period.', 'ai-discovery-files' ); ?></p>
    159159                <!-- Accessible data table (screen-reader only) -->
    160160                <table class="aidf-sr-only" id="aidf-chart-data-table">
     
    364364                    <?php esc_html_e( 'Save Settings', 'ai-discovery-files' ); ?>
    365365                </button>
     366                <button type="button" class="aidf-btn aidf-btn--secondary" id="aidf-rebuild-summary">
     367                    <?php esc_html_e( 'Rebuild Summary', 'ai-discovery-files' ); ?>
     368                </button>
    366369                <button type="button" class="aidf-btn aidf-btn--danger" id="aidf-clear-log">
    367370                    <?php esc_html_e( 'Clear Log Data', 'ai-discovery-files' ); ?>
    368371                </button>
    369372            </div>
     373            <p class="aidf-setting-row__help" style="margin-top: var(--aidf-sp-2);">
     374                <?php esc_html_e( 'The dashboard uses a pre-built summary for fast loading. It rebuilds automatically each day. Use "Rebuild Summary" if you have just enabled logging and want to see data immediately, or if the dashboard appears out of date.', 'ai-discovery-files' ); ?>
     375            </p>
    370376
    371377        </div>
  • ai-discovery-files/trunk/ai-discovery-files.php

    r3488558 r3488602  
    44 * Plugin URI:        https://www.ai-visibility.org.uk/wordpress-plugin/ai-discovery-files/
    55 * Description:       The only WordPress plugin that generates all 10 AI Discovery Files and tracks which AI bots visit your site. Built by 365i.
    6  * Version:           1.3.1
     6 * Version:           1.3.4
    77 * Requires at least: 6.2
    88 * Requires PHP:      8.0
     
    2424 * Plugin constants.
    2525 */
    26 define( 'AIDF_VERSION', '1.3.1' );
     26define( 'AIDF_VERSION', '1.3.4' );
    2727define( 'AIDF_PLUGIN_FILE', __FILE__ );
    2828define( 'AIDF_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
  • ai-discovery-files/trunk/includes/class-crawler-analytics.php

    r3488445 r3488602  
    437437                    : $count;
    438438            }
     439        }
     440
     441        // Pad with empty buckets so the chart always shows the full date range.
     442        // This ensures bars have proper width context even with sparse data.
     443        if ( ! $use_weekly ) {
     444            $padded = array();
     445            $date   = new \DateTime( 'now', new \DateTimeZone( 'UTC' ) );
     446            $date->modify( "-{$days} days" );
     447
     448            for ( $d = 0; $d < $days; $d++ ) {
     449                $date->modify( '+1 day' );
     450                $key = $date->format( 'Y-m-d' );
     451                $padded[ $key ] = isset( $chart[ $key ] ) ? $chart[ $key ] : new \stdClass();
     452            }
     453            $chart = $padded;
    439454        }
    440455
  • ai-discovery-files/trunk/readme.txt

    r3488558 r3488602  
    55Tested up to: 6.9
    66Requires PHP: 8.0
    7 Stable tag: 1.3.1
     7Stable tag: 1.3.4
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    179179== Changelog ==
    180180
     181= 1.3.4 =
     182* New: Replace CSS charts with Chart.js for polished, responsive visualisations
     183* New: Stacked bar chart with animated transitions, index tooltips, and proper scaling
     184* New: Bot detail trend chart rendered with Chart.js
     185
     186= 1.3.3 =
     187* New: Rebuild Summary button in Crawler Logging Settings for immediate dashboard updates
     188* Fix: Chart now shows full date range with proper bar sizing even with sparse data
     189
    181190= 1.3.1 =
    182191* Fix: Move bot detection into discovery file server for reliable CDN/cache compatibility
Note: See TracChangeset for help on using the changeset viewer.