| 1 |
/** |
| 2 |
* Stats page: Trend Analytics line charts. |
| 3 |
* |
| 4 |
* Uses Chart.js (bundled with the plugin at includes/js/lib/ and enqueued |
| 5 |
* as a hard dependency by AdminAssetEnqueuer), then fetches abj404getTrendData |
| 6 |
* for the selected period (7 / 30 / 90 days) and renders three line |
| 7 |
* charts: 404 hits, redirects, new captures. |
| 8 |
* |
| 9 |
* Reads ajaxUrl, nonce, and translatable labels from a JSON blob on |
| 10 |
* the configuration carrier element's `data-abj404-trends` attribute |
| 11 |
* (#abj404-trends-config). On 403 (expired nonce), refreshes via |
| 12 |
* window.abj404NonceRefresh and retries once. |
| 13 |
*/ |
| 14 |
(function () { |
| 15 |
'use strict'; |
| 16 |
|
| 17 |
function readConfig() { |
| 18 |
var el = document.getElementById('abj404-trends-config'); |
| 19 |
if (!el) { |
| 20 |
return null; |
| 21 |
} |
| 22 |
var raw = el.getAttribute('data-abj404-trends'); |
| 23 |
if (!raw) { |
| 24 |
return null; |
| 25 |
} |
| 26 |
try { |
| 27 |
return JSON.parse(raw); |
| 28 |
} catch (e) { |
| 29 |
// Malformed config JSON (encoding issue, WAF/proxy mangling the |
| 30 |
// attribute) previously left the panel stuck on "Loading chart |
| 31 |
// data..." forever with nothing in the console to diagnose. Log |
| 32 |
// for diagnosis and surface the same recoverable error panel |
| 33 |
// fetchAndRender()/loadChartJs() use for other failure modes. |
| 34 |
if (window.console && window.console.error) { |
| 35 |
window.console.error('404 Solution: abj404-trends-config data-abj404-trends attribute is not valid JSON', raw, e); |
| 36 |
} |
| 37 |
var loadEl = document.querySelector('.abj404-trends-loading'); |
| 38 |
if (loadEl) { loadEl.style.display = 'none'; } |
| 39 |
var errEl = document.getElementById('abj404-trends-error'); |
| 40 |
if (errEl) { errEl.style.display = ''; } |
| 41 |
return null; |
| 42 |
} |
| 43 |
} |
| 44 |
|
| 45 |
var cfg = null; |
| 46 |
var nonce = ''; |
| 47 |
var chartInstances = {}; |
| 48 |
|
| 49 |
// Trend data is a lighter read (three small aggregate series) than the |
| 50 |
// one-shot admin actions elsewhere in this bundle, so a shorter bound is |
| 51 |
// reasonable while still tolerating a slow-but-working shared host. A |
| 52 |
// stalled response must not hold the loading state / browser resources |
| 53 |
// open forever (M501). |
| 54 |
var TREND_DATA_TIMEOUT_MS = 20000; |
| 55 |
|
| 56 |
function loadChartJs(cb) { |
| 57 |
// Chart.js is bundled with the plugin and enqueued as a hard dependency |
| 58 |
// (see AdminAssetEnqueuer::addScripts), so window.Chart is already |
| 59 |
// defined by the time this runs. We dispatch abj404ChartJsLoaded so |
| 60 |
// statsConfidenceChart.js renders too, regardless of script order. |
| 61 |
if (window.Chart) { |
| 62 |
document.dispatchEvent(new Event('abj404ChartJsLoaded')); |
| 63 |
cb(); |
| 64 |
return; |
| 65 |
} |
| 66 |
// Defensive: if the bundled library somehow failed to load, surface the |
| 67 |
// trends error panel instead of silently rendering nothing. |
| 68 |
var loadEl = document.querySelector('.abj404-trends-loading'); |
| 69 |
if (loadEl) { loadEl.style.display = 'none'; } |
| 70 |
var errEl = document.getElementById('abj404-trends-error'); |
| 71 |
if (errEl) { errEl.style.display = ''; } |
| 72 |
} |
| 73 |
|
| 74 |
function buildChart(canvasId, label, color, labels, values) { |
| 75 |
var ctx = document.getElementById(canvasId); |
| 76 |
if (!ctx) { return null; } |
| 77 |
return new window.Chart(ctx, { |
| 78 |
type: 'line', |
| 79 |
data: { |
| 80 |
labels: labels, |
| 81 |
datasets: [{ |
| 82 |
label: label, |
| 83 |
data: values, |
| 84 |
borderColor: color, |
| 85 |
backgroundColor: color.replace('rgb(', 'rgba(').replace(')', ', 0.15)'), |
| 86 |
tension: 0.3, |
| 87 |
fill: true, |
| 88 |
pointRadius: 3 |
| 89 |
}] |
| 90 |
}, |
| 91 |
options: { |
| 92 |
responsive: true, |
| 93 |
plugins: { legend: { display: true } }, |
| 94 |
scales: { y: { beginAtZero: true, ticks: { precision: 0 } } } |
| 95 |
} |
| 96 |
}); |
| 97 |
} |
| 98 |
|
| 99 |
function getSelectedDays() { |
| 100 |
var radios = document.querySelectorAll('input[name=abj404_trend_period]'); |
| 101 |
for (var i = 0; i < radios.length; i++) { |
| 102 |
if (radios[i].checked) { |
| 103 |
return parseInt(radios[i].value, 10); |
| 104 |
} |
| 105 |
} |
| 106 |
return 30; |
| 107 |
} |
| 108 |
|
| 109 |
function destroyCharts() { |
| 110 |
['abj404-chart-404s', 'abj404-chart-redirects', 'abj404-chart-captures'].forEach(function (id) { |
| 111 |
if (chartInstances[id]) { |
| 112 |
chartInstances[id].destroy(); |
| 113 |
delete chartInstances[id]; |
| 114 |
} |
| 115 |
}); |
| 116 |
} |
| 117 |
|
| 118 |
function fetchTrendData(days, allowRetry) { |
| 119 |
// Each call (including the nonce-refresh retry below) gets its own |
| 120 |
// bounded controller so a stalled admin-ajax response cannot hold |
| 121 |
// the loading state open forever (M501). Abort surfaces as a |
| 122 |
// rejection that flows through the same generic .catch() in |
| 123 |
// fetchAndRender() already used for every other fetch failure. |
| 124 |
var controller = new AbortController(); |
| 125 |
var timeoutId = setTimeout(function () { controller.abort(); }, TREND_DATA_TIMEOUT_MS); |
| 126 |
// ajax-direct-approved: trend chart endpoint streams a GET response and owns nonce-refresh retry handling locally. |
| 127 |
return fetch(cfg.ajaxUrl + '?action=abj404getTrendData&nonce=' + encodeURIComponent(nonce) + '&days=' + days, { signal: controller.signal }) // allow-direct-network: trend chart endpoint; no project-wide adapter exists for this AJAX surface |
| 128 |
.then(function (r) { |
| 129 |
clearTimeout(timeoutId); |
| 130 |
// B20: a 12-24h-idle nonce expires; admin-ajax replies 403. |
| 131 |
// Mint a fresh nonce via the shared refresh helper (if loaded) |
| 132 |
// and retry once. allowRetry guards against an infinite loop. |
| 133 |
if (r.status === 403 && allowRetry !== false && window.abj404NonceRefresh) { |
| 134 |
return window.abj404NonceRefresh.fetchFresh().then(function (freshNonces) { |
| 135 |
if (freshNonces && freshNonces['abj404_trendData']) { |
| 136 |
nonce = freshNonces['abj404_trendData']; |
| 137 |
} |
| 138 |
return fetchTrendData(days, false); |
| 139 |
}); |
| 140 |
} |
| 141 |
return r.json(); |
| 142 |
}) |
| 143 |
.catch(function (e) { |
| 144 |
clearTimeout(timeoutId); |
| 145 |
throw e; |
| 146 |
}); |
| 147 |
} |
| 148 |
|
| 149 |
function fetchAndRender() { |
| 150 |
var days = getSelectedDays(); |
| 151 |
var loadEl = document.querySelector('.abj404-trends-loading'); |
| 152 |
var errEl = document.getElementById('abj404-trends-error'); |
| 153 |
var chartsEl = document.getElementById('abj404-trends-charts'); |
| 154 |
if (loadEl) { loadEl.style.display = ''; } |
| 155 |
if (errEl) { errEl.style.display = 'none'; } |
| 156 |
if (chartsEl) { chartsEl.style.display = 'none'; } |
| 157 |
destroyCharts(); |
| 158 |
fetchTrendData(days, true) |
| 159 |
.then(function (resp) { |
| 160 |
if (loadEl) { loadEl.style.display = 'none'; } |
| 161 |
if (!resp || !resp.success || !Array.isArray(resp.data)) { |
| 162 |
if (errEl) { errEl.style.display = ''; } |
| 163 |
return; |
| 164 |
} |
| 165 |
var rows = resp.data; |
| 166 |
var labels = rows.map(function (r) { return r.date; }); |
| 167 |
var vals404 = rows.map(function (r) { return r.hits_404; }); |
| 168 |
var valsRedir = rows.map(function (r) { return r.hits_redirect; }); |
| 169 |
var valsCapt = rows.map(function (r) { return r.new_captures; }); |
| 170 |
if (chartsEl) { chartsEl.style.display = ''; } |
| 171 |
// allow-hardcoded-color: Chart.js dataset border colors must be JS string literals; |
| 172 |
// Chart.js cannot read CSS custom properties (--abj404-*) from a canvas context. |
| 173 |
chartInstances['abj404-chart-404s'] = buildChart('abj404-chart-404s', cfg.label404, 'rgb(0,115,170)', labels, vals404); |
| 174 |
chartInstances['abj404-chart-redirects'] = buildChart('abj404-chart-redirects', cfg.labelRedirect, 'rgb(70,170,100)', labels, valsRedir); |
| 175 |
chartInstances['abj404-chart-captures'] = buildChart('abj404-chart-captures', cfg.labelCapture, 'rgb(220,100,50)', labels, valsCapt); |
| 176 |
}) |
| 177 |
.catch(function () { |
| 178 |
if (loadEl) { loadEl.style.display = 'none'; } |
| 179 |
if (errEl) { errEl.style.display = ''; } |
| 180 |
}); |
| 181 |
} |
| 182 |
|
| 183 |
function onPeriodChange() { fetchAndRender(); } |
| 184 |
|
| 185 |
document.addEventListener('DOMContentLoaded', function () { |
| 186 |
cfg = readConfig(); |
| 187 |
if (!cfg) { |
| 188 |
return; |
| 189 |
} |
| 190 |
nonce = cfg.nonce || ''; |
| 191 |
loadChartJs(fetchAndRender); |
| 192 |
document.querySelectorAll('input[name=abj404_trend_period]').forEach(function (r) { |
| 193 |
r.addEventListener('change', onPeriodChange); |
| 194 |
}); |
| 195 |
}); |
| 196 |
})(); |
| 197 |
|