File: /home/mahdetej/public_html/wp-content/plugins/Melli-shild/assets/js/performance.js
/**
* MelliShield Performance Analyzer
*
* @package Melli_Shield
* @since 1.0.0
*/
(function ($) {
'use strict';
var Perf = {
resources: [],
checked: 0,
total: 0,
running: false,
init: function () {
if ($('#melli-shield-perf-start').length === 0 &&
$('#melli-shield-perf-client').length === 0 &&
$('#melli-shield-frontend-results').length === 0) {
return;
}
this.bind();
// Auto-load frontend data if spinner present
if ($('#melli-shield-frontend-results .spinner').length > 0) {
this.loadFrontendData();
}
},
bind: function () {
var self = this;
$('#melli-shield-perf-start').on('click', function (e) { e.preventDefault(); self.startServer(); });
$('#melli-shield-perf-client').on('click', function (e) { e.preventDefault(); self.collectClient(); });
},
// ========================================
// Server-Side Analysis
// ========================================
startServer: function () {
if (this.running) return;
this.running = true;
this.resources = [];
this.checked = 0;
this.total = 0;
var url = $('#melli-shield-perf-url').val() || '';
this.status('Extracting resources...', 'active');
this.disableButtons(true);
$('#melli-shield-perf-results').html('<p class="msh-text-muted"><span class="msh-spinner"></span> Extracting...</p>');
var self = this;
$.ajax({
url: melli_shield_ajax.ajax_url, type: 'POST', dataType: 'json',
data: { action: 'melli_shield_perf_extract', page_url: url, _wpnonce: melli_shield_ajax.nonce },
success: function (r) {
if (r.success && r.data.resources) {
self.resources = r.data.resources;
self.total = self.resources.length;
if (self.total === 0) {
$('#melli-shield-perf-results').html('<div class="msh-empty"><p>منبعی یافت نشد.</p></div>');
self.done(); return;
}
self.renderTable(r.data);
self.status('Checking URLs...', 'active');
self.checkNext();
} else {
self.done();
MelliShield.showNotice(r.data && r.data.message ? r.data.message : 'Error', 'error');
}
},
error: function (xhr, st) { self.done(); MelliShield.showNotice('AJAX error: ' + st, 'error'); }
});
},
checkNext: function () {
if (this.checked >= this.total) { this.serverComplete(); return; }
var res = this.resources[this.checked];
var $row = $('#perf-row-' + this.checked);
$row.find('.p-status').html('<span class="msh-status-pending">⏳</span>');
this.progress(Math.round((this.checked / this.total) * 100));
var self = this;
$.ajax({
url: melli_shield_ajax.ajax_url, type: 'POST', dataType: 'json',
data: { action: 'melli_shield_perf_check_url', url: res.url, _wpnonce: melli_shield_ajax.nonce },
success: function (r) {
if (r.success) self.renderRow(self.checked, r.data);
else $row.find('.p-status').html('<span class="msh-status-error">❌</span>');
self.checked++;
setTimeout(function () { self.checkNext(); }, 50);
},
error: function () {
$row.find('.p-status').html('<span class="msh-status-error">❌</span>');
self.checked++;
setTimeout(function () { self.checkNext(); }, 50);
}
});
},
renderTable: function (data) {
var h = '<div class="msh-card"><div id="perf-progress" class="msh-progress msh-progress--labeled"><div id="perf-bar" class="msh-progress__bar" style="width:0%">0%</div></div>';
h += '<p id="perf-text" class="msh-text-xs msh-text-muted msh-mt-sm"></p></div>';
h += '<p class="msh-card__hint"><strong>' + data.total + '</strong> منبع | HTML: ' + MelliShield.formatBytes(data.html_size) + '</p>';
h += '<div class="msh-table-wrap"><table class="msh-table"><thead><tr><th>#</th><th>URL</th><th>نوع</th><th>دامنه</th><th>زمان</th><th>وضعیت</th><th>حجم</th><th>کش</th><th>رتبه</th></tr></thead><tbody>';
for (var i = 0; i < data.resources.length; i++) {
var r = data.resources[i], dom = this.domain(r.url);
h += '<tr id="perf-row-' + i + '"><td>' + (i + 1) + '</td>';
h += '<td><code style="font-size:11px;word-break:break-all;">' + MelliShield.truncate(r.url, 80) + '</code></td>';
h += '<td><span class="melli-shield-badge melli-shield-badge-' + MelliShield.escapeHtml(r.type) + '">' + MelliShield.escapeHtml(r.type) + '</span></td>';
h += '<td><code style="font-size:11px;">' + MelliShield.escapeHtml(dom) + '</code></td>';
h += '<td class="p-time">—</td><td class="p-status"><span class="msh-status-pending">⏳</span></td>';
h += '<td class="p-size">—</td><td class="p-cache">—</td><td class="p-rating">—</td></tr>';
}
h += '</tbody></table></div>';
$('#melli-shield-perf-results').html(h);
},
renderRow: function (i, d) {
var $r = $('#perf-row-' + i);
if (!$r.length) return;
var tc = this.timingClass(d.response_time, 500, 1000);
$r.find('.p-time').html('<span class="' + tc + '">' + d.response_time + 'ms</span>');
if (d.status === 'error') {
$r.find('.p-status').html('<span class="msh-status-error" title="' + MelliShield.escapeHtml(d.error) + '">❌ ' + (d.is_blocked ? 'Blocked' : 'Error') + '</span>');
} else {
var sc = (d.status_code >= 200 && d.status_code < 400) ? 'msh-text-success' : 'msh-text-danger';
$r.find('.p-status').html('<span class="' + sc + '">' + d.status_code + '</span>');
}
$r.find('.p-size').text(d.content_length > 0 ? MelliShield.formatBytes(d.content_length) : '—');
if (d.cache_control) {
var cached = d.cache_control.indexOf('max-age') !== -1 || d.cache_control.indexOf('public') !== -1;
$r.find('.p-cache').html(cached ? '<span class="msh-text-success">✓</span>' : '<span class="msh-text-danger">✗</span>');
}
var rm = { fast: '🟢 Fast', medium: '🟡 Medium', slow: '🔴 Slow' };
var rc = { fast: 'msh-text-success', medium: 'msh-text-warn', slow: 'msh-text-danger' };
$r.find('.p-rating').html('<span class="' + (rc[d.rating] || 'msh-text-muted') + '">' + (rm[d.rating] || '—') + '</span>');
$r.removeClass('msh-row-danger msh-row-warn msh-row-external');
if (d.is_blocked || d.status === 'error') $r.addClass('msh-row-danger');
else if (d.is_external && d.response_time > 1000) $r.addClass('msh-row-warn');
else if (d.is_external) $r.addClass('msh-row-external');
},
serverComplete: function () {
this.progress(100); this.done();
var slow = 0, fast = 0, med = 0, err = 0;
$('#melli-shield-perf-results table tbody tr').each(function () {
var t = parseInt($(this).find('.p-time span').text(), 10);
if (!isNaN(t)) { if (t > 1000) slow++; else if (t > 500) med++; else fast++; }
var st = $(this).find('.p-status').text();
if (st.indexOf('Blocked') !== -1 || st.indexOf('Error') !== -1) err++;
});
var sh = '<div class="msh-stats" style="margin:15px 0;">';
sh += this.card('کل', this.total) + this.card('سریع', fast, 'success');
sh += this.card('متوسط', med, 'warn') + this.card('کند', slow, 'danger');
sh += this.card('خطا', err, err > 0 ? 'danger' : 'success') + '</div>';
if (slow > 0 || err > 0) {
sh += '<div class="msh-notice msh-notice--warning" style="margin-bottom:15px;"><p><strong>پیشنهاد:</strong></p><ul style="margin:6px 20px 0 0;line-height:1.8;">';
if (slow > 0) sh += '<li>' + slow + ' منبع کند شناسایی شد.</li>';
if (err > 0) sh += '<li>' + err + ' منبع مسدود یا خطادار.</li>';
sh += '</ul></div>';
} else {
sh += '<div class="msh-notice msh-notice--success" style="margin-bottom:15px;"><p><strong>همه منابع سریع هستند.</strong></p></div>';
}
$('#perf-progress').after(sh);
MelliShield.showNotice('Complete: ' + fast + ' fast, ' + med + ' medium, ' + slow + ' slow, ' + err + ' errors.', (slow > 0 || err > 0) ? 'warning' : 'success');
},
// ========================================
// Admin Page Client-Side
// ========================================
collectClient: function () {
if (!window.performance || !window.performance.getEntriesByType) {
MelliShield.showNotice('Resource Timing API not supported.', 'error'); return;
}
var entries = performance.getEntriesByType('resource'), data = [];
for (var i = 0; i < entries.length; i++) {
var e = entries[i];
data.push({
url: e.name, type: e.initiatorType || 'other', duration: Math.round(e.duration),
dns: Math.round(e.domainLookupEnd - e.domainLookupStart),
connect: Math.round(e.connectEnd - e.connectStart),
ttfb: Math.round(e.responseStart - e.requestStart),
download: Math.round(e.responseEnd - e.responseStart),
size: e.transferSize || 0, cached: e.transferSize === 0 && e.decodedBodySize > 0
});
}
data.sort(function (a, b) { return b.duration - a.duration; });
if (data.length === 0) {
$('#melli-shield-perf-client-results').html('<div class="msh-empty"><p>دادهای در دسترس نیست.</p></div>'); return;
}
var $c = $('#melli-shield-perf-client-results');
$c.html('');
this.renderClientInto(data, $c);
$.ajax({
url: melli_shield_ajax.ajax_url, type: 'POST',
data: { action: 'melli_shield_perf_save_client', entries: JSON.stringify(data), _wpnonce: melli_shield_ajax.nonce },
success: function (r) { if (r.success) MelliShield.showNotice('Data saved (' + data.length + ' resources).', 'success'); }
});
},
// ========================================
// Frontend Data Loader
// ========================================
loadFrontendData: function () {
var self = this;
$.ajax({
url: melli_shield_ajax.ajax_url, type: 'POST', dataType: 'json',
data: { action: 'melli_shield_perf_get_frontend', _wpnonce: melli_shield_ajax.nonce },
success: function (r) {
if (r.success && r.data.entries) {
var meta = r.data.meta || {}, entries = r.data.entries;
var info = '<div class="msh-card" style="margin-bottom:18px;">';
info += '<div style="display:flex;gap:20px;flex-wrap:wrap;font-size:13px;color:var(--msh-muted);">';
if (meta.url) info += '<span><strong>صفحه:</strong> <code>' + MelliShield.escapeHtml(meta.url) + '</code></span>';
if (meta.title) info += '<span><strong>عنوان:</strong> ' + MelliShield.escapeHtml(meta.title) + '</span>';
if (r.data.collected) info += '<span><strong>جمعآوری:</strong> ' + MelliShield.escapeHtml(r.data.collected) + '</span>';
info += '<span><strong>منابع:</strong> ' + entries.length + '</span></div>';
if (meta.page_timing && meta.page_timing.load_event) {
var pt = meta.page_timing;
info += '<div class="msh-stats" style="margin-top:14px;">';
info += self.card('DNS', pt.dns + 'ms', pt.dns > 100 ? 'danger' : 'success');
info += self.card('TTFB', pt.ttfb + 'ms', pt.ttfb > 500 ? 'danger' : 'success');
info += self.card('DOM', pt.dom_interactive + 'ms', pt.dom_interactive > 3000 ? 'danger' : 'warn');
info += self.card('بارگذاری', pt.load_event + 'ms', pt.load_event > 5000 ? 'danger' : 'success');
info += '</div>';
}
info += '</div>';
var $c = $('#melli-shield-frontend-results');
$c.html(info);
self.renderClientInto(entries, $c);
} else {
var msg = (r.data && r.data.message) ? r.data.message : 'No data found.';
$('#melli-shield-frontend-results').html(
'<div class="msh-empty"><p>' + MelliShield.escapeHtml(msg) + '</p>' +
'<p style="margin-top:8px;font-size:13px;color:var(--msh-muted);">به یک صفحه عمومی بروید و از نوار ادمین: <strong>ملی شیلد → تحلیل این صفحه</strong></p></div>');
}
},
error: function () {
$('#melli-shield-frontend-results').html('<div class="msh-notice msh-notice--error"><p>خطا در بارگذاری داده.</p></div>');
}
});
},
// ========================================
// Shared Client Renderer
// ========================================
renderClientInto: function (data, $container) {
var site = window.location.hostname;
var totalSize = 0, slowCount = 0, cachedCount = 0, extCount = 0;
for (var s = 0; s < data.length; s++) {
totalSize += data[s].size || 0;
if (data[s].duration > 1000) slowCount++;
if (data[s].cached) cachedCount++;
try { if (new URL(data[s].url).hostname !== site) extCount++; } catch (e) { }
}
var h = '';
// Summary cards
h += '<div class="msh-stats" style="margin-bottom:18px;">';
h += this.card('منابع', data.length);
h += this.card('حجم کل', MelliShield.formatBytes(totalSize), 'primary');
h += this.card('کند (>۱ث)', slowCount, slowCount > 0 ? 'danger' : 'success');
h += this.card('کششده', cachedCount, 'primary');
h += this.card('خارجی', extCount, extCount > 0 ? 'warn' : 'success');
h += '</div>';
if (slowCount > 0 || extCount > 5) {
h += '<div class="msh-notice msh-notice--warning" style="margin-bottom:15px;">';
h += '<p><strong>پیشنهاد:</strong></p><ul style="margin:6px 20px 0 0;line-height:1.8;">';
if (slowCount > 0) h += '<li>' + slowCount + ' منبع بیش از ۱ ثانیه لود میشود.</li>';
if (extCount > 5) h += '<li>' + extCount + ' منبع خارجی — حالت ملی/آفلاین را در نظر بگیرید.</li>';
if (cachedCount < data.length * 0.3) h += '<li>نرخ کش پایین است؛ کش مرورگر را فعال کنید.</li>';
h += '</ul></div>';
} else {
h += '<div class="msh-notice msh-notice--success" style="margin-bottom:15px;">';
h += '<p><strong>عملکرد خوب است.</strong></p></div>';
}
h += '<h3 class="msh-perf-section-title">جزئیات منابع (بر اساس مدت)</h3>';
h += '<div class="msh-table-wrap"><table class="msh-table"><thead><tr>';
h += '<th>#</th><th>URL</th><th>Type</th><th>DNS</th><th>Connect</th><th>TTFB</th><th>Download</th><th>Total</th><th>Size</th><th>Cached</th>';
h += '</tr></thead><tbody>';
var max = Math.min(data.length, 100);
for (var i = 0; i < max; i++) {
var e = data[i];
var tc = this.timingClass(e.duration, 500, 1000);
var rowClass = e.duration > 1000 ? 'msh-row-danger' : (e.duration > 500 ? 'msh-row-warn' : '');
var ext = false;
try { ext = new URL(e.url).hostname !== site; } catch (ex) { }
h += '<tr class="' + rowClass + '">';
h += '<td>' + (i + 1) + '</td>';
h += '<td><code class="msh-text-xs" style="word-break:break-all;">' + MelliShield.truncate(e.url, 70) + '</code>';
if (ext) h += ' <span class="msh-text-warn msh-text-xs">⬆ ext</span>';
h += '</td>';
h += '<td class="msh-text-xs">' + MelliShield.escapeHtml(e.type) + '</td>';
h += '<td>' + this.colorMs(e.dns, 50) + '</td>';
h += '<td>' + this.colorMs(e.connect, 100) + '</td>';
h += '<td>' + this.colorMs(e.ttfb, 200) + '</td>';
h += '<td>' + this.colorMs(e.download, 300) + '</td>';
h += '<td><span class="' + tc + '">' + e.duration + 'ms</span></td>';
h += '<td>' + (e.size > 0 ? MelliShield.formatBytes(e.size) : '—') + '</td>';
h += '<td>' + (e.cached ? '<span class="msh-text-primary">✓</span>' : '—') + '</td>';
h += '</tr>';
}
if (data.length > 100) {
h += '<tr><td colspan="10" class="msh-text-muted" style="text-align:center;">Showing top 100 of ' + data.length + '</td></tr>';
}
h += '</tbody></table></div>';
// Waterfall chart
h += this.waterfall(data.slice(0, 20));
// External domains summary
var domains = {};
for (var d = 0; d < data.length; d++) {
try {
var dom = new URL(data[d].url).hostname;
if (dom !== site) {
if (!domains[dom]) domains[dom] = { count: 0, totalTime: 0, totalSize: 0 };
domains[dom].count++;
domains[dom].totalTime += data[d].duration;
domains[dom].totalSize += data[d].size || 0;
}
} catch (ex) { }
}
var domArr = [];
for (var dk in domains) {
if (domains.hasOwnProperty(dk)) {
domArr.push({ domain: dk, count: domains[dk].count, time: domains[dk].totalTime, size: domains[dk].totalSize });
}
}
domArr.sort(function (a, b) { return b.time - a.time; });
if (domArr.length > 0) {
h += '<h3 class="msh-perf-section-title">خلاصه دامنههای خارجی</h3>';
h += '<div class="msh-table-wrap"><table class="msh-table"><thead><tr>';
h += '<th>دامنه</th><th>درخواست</th><th>زمان کل</th><th>حجم</th><th>میانگین</th><th>وضعیت</th>';
h += '</tr></thead><tbody>';
for (var di = 0; di < domArr.length; di++) {
var dd = domArr[di];
var avg = Math.round(dd.time / dd.count);
var avgClass = this.timingClass(avg, 200, 500);
h += '<tr>';
h += '<td><code>' + MelliShield.escapeHtml(dd.domain) + '</code></td>';
h += '<td>' + dd.count + '</td>';
h += '<td>' + dd.time + 'ms</td>';
h += '<td>' + MelliShield.formatBytes(dd.size) + '</td>';
h += '<td><span class="' + avgClass + '">' + avg + 'ms</span></td>';
h += '<td>';
if (avg > 500) h += '<span class="msh-text-danger">🔴 Slow — consider blocking</span>';
else if (avg > 200) h += '<span class="msh-text-warn">🟡 Medium</span>';
else h += '<span class="msh-text-success">🟢 Fast</span>';
h += '</td></tr>';
}
h += '</tbody></table></div>';
}
$container.append(h);
},
// ========================================
// Waterfall Chart
// ========================================
waterfall: function (data) {
if (!data || data.length === 0) return '';
var maxD = data[0].duration || 1;
var h = '<h3 class="msh-perf-section-title">📊 Waterfall — Top 20 Slowest</h3>';
h += '<div class="melli-shield-waterfall">';
for (var i = 0; i < data.length; i++) {
var e = data[i];
var tw = Math.max(2, Math.round((e.duration / maxD) * 100));
var dw = Math.max(0, Math.round((e.dns / maxD) * 100));
var cw = Math.max(0, Math.round((e.connect / maxD) * 100));
var bw = Math.max(0, Math.round((e.ttfb / maxD) * 100));
var lw = Math.max(1, tw - dw - cw - bw);
if (lw < 0) lw = 1;
var tc = this.timingClass(e.duration, 500, 1000);
h += '<div class="melli-shield-waterfall-row">';
h += '<div class="melli-shield-waterfall-label" title="' + MelliShield.escapeHtml(e.url) + '"><code class="msh-text-xs">' + MelliShield.escapeHtml(MelliShield.truncate(e.url, 40)) + '</code></div>';
h += '<div class="melli-shield-waterfall-bar">';
if (dw > 0) h += '<div class="msh-waterfall-seg-dns" style="width:' + dw + '%;" title="DNS: ' + e.dns + 'ms"></div>';
if (cw > 0) h += '<div class="msh-waterfall-seg-connect" style="width:' + cw + '%;" title="Connect: ' + e.connect + 'ms"></div>';
if (bw > 0) h += '<div class="msh-waterfall-seg-ttfb" style="width:' + bw + '%;" title="TTFB: ' + e.ttfb + 'ms"></div>';
if (lw > 0) h += '<div class="msh-waterfall-seg-download" style="width:' + lw + '%;" title="Download: ' + e.download + 'ms"></div>';
h += '</div>';
h += '<div class="melli-shield-waterfall-time ' + tc + '">' + e.duration + 'ms</div>';
h += '</div>';
}
// Scale
h += '<div style="display:flex;align-items:center;margin-top:8px;font-size:10px;color:#999;">';
h += '<div style="width:250px;"></div>';
h += '<div style="flex:1;display:flex;justify-content:space-between;">';
h += '<span>0ms</span><span>' + Math.round(maxD * 0.5) + 'ms</span><span>' + maxD + 'ms</span>';
h += '</div><div style="width:65px;"></div></div>';
// Legend
h += '<div style="display:flex;gap:15px;margin-top:12px;padding-top:10px;border-top:1px solid #e0e0e0;font-size:11px;color:#666;">';
h += '<span><span style="display:inline-block;width:12px;height:12px;background:#9b59b6;border-radius:2px;vertical-align:middle;margin-right:3px;"></span> DNS</span>';
h += '<span><span style="display:inline-block;width:12px;height:12px;background:#e67e22;border-radius:2px;vertical-align:middle;margin-right:3px;"></span> Connect</span>';
h += '<span><span style="display:inline-block;width:12px;height:12px;background:#3498db;border-radius:2px;vertical-align:middle;margin-right:3px;"></span> TTFB</span>';
h += '<span><span style="display:inline-block;width:12px;height:12px;background:#2ecc71;border-radius:2px;vertical-align:middle;margin-right:3px;"></span> Download</span>';
h += '</div></div>';
return h;
},
// ========================================
// UI Helpers
// ========================================
card: function (label, value, tone) {
tone = tone || '';
var toneClass = tone ? ' msh-stat-card--' + tone : '';
return '<div class="msh-stat-card ms-stat' + toneClass + '" style="min-width:110px;">' +
'<div class="msh-stat-card__label ms-stat__label">' + MelliShield.escapeHtml(String(label)) + '</div>' +
'<div class="msh-stat-card__value ms-stat__num">' + MelliShield.escapeHtml(String(value)) + '</div></div>';
},
timingClass: function (ms, warnAt, dangerAt) {
if (ms <= 0) return 'msh-text-muted';
if (ms > dangerAt) return 'msh-text-danger';
if (ms > warnAt) return 'msh-text-warn';
return 'msh-text-success';
},
colorMs: function (ms, threshold) {
return '<span class="' + this.timingClass(ms, threshold / 2, threshold) + '">' + ms + 'ms</span>';
},
progress: function (pct) {
pct = Math.min(100, Math.max(0, pct));
$('#perf-bar').css('width', pct + '%').text(pct + '%');
$('#perf-text').text('در حال بررسی ' + this.checked + ' / ' + this.total + '...');
},
status: function (text, state) {
var $el = $('#melli-shield-perf-status');
$el.text(text).removeClass('is-active is-success is-warn is-error msh-text-muted');
if (state) {
$el.addClass('msh-status-text is-' + state);
} else {
$el.addClass('msh-status-text');
}
},
disableButtons: function (disabled) {
$('#melli-shield-perf-start, #melli-shield-perf-client').prop('disabled', disabled);
},
done: function () {
this.running = false;
this.disableButtons(false);
this.status('✓ Complete', 'success');
},
domain: function (url) {
try { var a = document.createElement('a'); a.href = url; return a.hostname; }
catch (e) { return ''; }
}
};
$(document).ready(function () {
Perf.init();
});
})(jQuery);