
لقد مرّ أكثر من عامين منذ أن توقفتُ عن استخدام Google Analytics وانتقلتُ إلى Matomo On-Premise (مستضاف ذاتيًا). وسعيد للغاية.
إذا كنت تستخدم Matomo On-Premiseفأنت تعرف بالفعل أنه أداة قوية ومتكاملة، ولكن في كل مرة تريد فيها الاطلاع على الإحصائيات الأساسية يكون الأمر غير مريح وربما يتطلب الكثير من الجهد على الخادم الخاص بك، على الرغم من أن أداءه قد تحسن كثيرًا مقارنةً بأيامه الأولى عندما كان يُسمى Piwic، والذي كان يلتهم الخادم بالاستعلامات على الفور.
هناك خطط تكامل ماتومو Matomo، لكنها تعاني من مشكلتين: إما أنها تعرض إطارات iframes محظورة لأسباب أمنية، أو أنها تقوم بإجراء استعلامات PHP متزامنة تبطئ لوحة الإدارة.
الأداة المسحوبة من واجهة برمجة تطبيقات Matomo
لقد شرعتُ في إنشاء أداة أصلية لسطح مكتب ووردبريس باستخدام واجهة برمجة تطبيقات Matomo التي ستكون خفيفة الوزن وممتعة من الناحية الجمالية وقبل كل شيء في الوقت الفعلي، ولكن ذلك لن يؤثر على الخادم وسيعمل كل شيء في متصفح العميل. للقيام بذلك، اعتمدتُ على وثائق Matomo ومساعدة Gemini 3.0، والتي تم تحسينها كثيرًا في نسختها التي صدرت مؤخرًا من حيث التعامل مع الشيفرة ولأنني لا أستطيع كتابة أكثر من ثلاثة أسطر دون كسر شيء ما.
تحذير: الأداة مصممة للتثبيتات المحلية (نفس الخادم أو خادمك الخاص)، على الرغم من أنه نظرًا لأنها JS، ستعمل حتى إذا كان لديك Matomo على نطاق آخر إذا سمحت أذونات المتصفح (CORS) بذلك.
بعد شرح وظائفه وإضافته، ستجد الكود الكامل الذي يمكنك إضافته إلى القالب أو القالب الفرعي أو المقتطفات الخاصة بك مع وظائف الإضافة function.php وإضافة عنوان URL الخاص بك ومعرف مشروعك ورمز Matomo Auth Token.
البيانات الأساسية في الوقت الحقيقي
نظرًا لأنني لا أحتاج إلى الكثير من القياسات في الأداة، فقد قمت بإعدادها بثلاثة مربعات رئيسية أتحقق منها بانتظام.
الأول هو عداد الزوار عبر الإنترنت، والذي يتم تحديثه تلقائياً كل عشر ثوانٍ.
يوجد مربعان في الأسفل يحتويان على العدد الإجمالي للزيارات لليوم، وعلى اليمين مربع "الأنشطة"، لليوم أيضًا، والذي يسميه ماتومو "مرات الظهور" ولكنه في الواقع ليس دقيقًا تمامًا لأنه يضيف أيضًا أنشطة أخرى مثل فتح صورة أو إجراء بحث أو تشغيل حدث بنقرة أو النقر على رابط إلى عنوان URL خارجي. يعتمد كلا الصندوقين على 30 يومًا من البيانات التاريخية وحقن البيانات في الوقت الفعلي.

الرسم البياني للزيارات هو المربع الوحيد الذي لا يتم تحديثه في الوقت الفعلي، حيث لا فائدة من فرض استعلامات غير ضرورية للبيانات التي لدينا بالفعل في مربع "الزيارات (اليوم)"، وتم تضمين زر تحديث للسماح بتحديث الرسم البياني مرة واحدة فقط في أي وقت. فيما يلي رابط لتنزيل ملف CSV أو صورة للرسم البياني لآخر 30 يومًا.
بالنسبة لهذا الرسم البياني، نستخدم مكتبة Chart.js، التي تسمح برسم رسم بياني بدلاً من صورة ثابتة، مما يسمح بالتفاعل وإمكانية رؤية البيانات يومًا بعد يوم، نقطة بنقطة.

المربع الرابع عبارة عن سجل مفصل للزائرين يعرض: اللافتات والمتصفحات وأنظمة التشغيل ورابط لتقرير الملف الشخصي للزائر. يتم تمثيل جميع الأنشطة بشكل مرئي مع أيقونات مقابلة للزيارات والتنزيلات (للصور أو ملفات pdf أو مستندات أخرى)، والنقرات على الروابط الخارجية، وعمليات البحث الداخلية للموقع والأحداث.

تحتوي الأيقونات على تلميحات أدوات "غنية" على شكل مربعات سوداء تحاكي أسلوب ماتومو وتحتوي على الحد الأدنى من المعلومات ذات الصلة.


التدابير الواجب اتخاذها
1 - احصل على رمز ماتومو الرمزي الخاص بك.
تحتاج إلى "مفتاح" حتى يتمكن WordPress الخاص بك من "التواصل" مع Matomo الخاص بك:
- أدخل ماتومو الخاص بك.
- انتقل إلى الإدارة (الترس) > الموظفين > الأمن.
- أنشئ رمز مصادقة جديد. انسخ هذا التسلسل من الأحرف. دوِّنه على الفور واحفظه، لأنه سيظهر مرة واحدة فقط، وإذا فقدته، سيتعين عليك إنشاء رمز جديد.
تحتاج إلى التأكد من أن المربعين اللذين يظهران غير محددين، إذا كانا محددين فقم بإلغاء تحديدهما بحيث يظهران كما هو موضح في الصورة:

2. التكوين
بعد إضافة الكود إلى function.phpالخاص بك، ما عليك سوى تعديل هذه الأسطر الثلاثة // ---- CONFIGIGURATION --- لتضمين بياناتك:
$matomo_url = 'https://tu-dominio.com/matomo/'; // La URL donde tienes instalado Matomo
$id_site = '1'; // El ID de tu sitio (suele ser 1)
$token_auth = 'PEGA_AQUI_TU_TOKEN_DE_32_CARACTERES';3- الكود
/**
* Widget Matomo V25 - Dashboard Matomo Tiempo Real + Tooltip + Gráfica
*/
if ( ! function_exists( 'jrmora_widget_matomo_v25' ) ) {
function jrmora_widget_matomo_v25() {
wp_add_dashboard_widget(
'jrmora_matomo_widget_v25',
'Estadísticas Matomo (En Vivo)',
'jrmora_render_matomo_v25'
);
}
add_action( 'wp_dashboard_setup', 'jrmora_widget_matomo_v25' );
function jrmora_render_matomo_v25() {
// --- CONFIGURACIÓN ---
$matomo_url = 'https://tu-dominio.com/matomo/'; // La URL donde tienes instalado Matomo
$id_site = '1'; // El ID de tu sitio (suele ser 1)
$token_auth = 'PEGA_AQUI_TU_TOKEN_DE_32_CARACTERES';
// ---------------------
$export_csv = $matomo_url . "index.php?module=API&method=VisitsSummary.get&idSite=$id_site&period=day&date=last30&format=CSV&token_auth=$token_auth";
$export_img = $matomo_url . "index.php?module=API&method=ImageGraph.get&idSite=$id_site&apiModule=VisitsSummary&apiAction=get&token_auth=$token_auth&graphType=evolution&period=day&date=last30&width=800&height=400";
?>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
.matomo-js-wrapper { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; position: relative; min-height: 250px; }
.status-bar { text-align: right; font-size: 11px; color: #888; margin-bottom: 10px; margin-top: -30px; }
.loading-dot { display: inline-block; width: 8px; height: 8px; background-color: #ccc; border-radius: 50%; margin-right: 5px; transition: background 0.3s; }
.loading-dot.active { background-color: #2271b1; box-shadow: 0 0 5px #2271b1; }
.hero-box { background: #fff; border: 1px solid #ccd0d4; border-left: 4px solid #2271b1; padding: 20px; text-align: center; margin-bottom: 15px; box-shadow: 0 1px 2px rgba(0,0,0,0.05); }
.hero-num { font-size: 64px; font-weight: 700; color: #1d2327; line-height: 1; display: block; margin-bottom: 5px; }
.hero-desc { font-size: 13px; color: #646970; }
.kpi-grid { display: flex; gap: 10px; margin-bottom: 15px; }
.kpi-card { background: #fff; border: 1px solid #ccd0d4; padding: 10px; flex: 1; text-align: center; }
.kpi-card h4 { margin: 0; font-size: 11px; color: #666; text-transform: uppercase; }
.kpi-card .val { font-size: 22px; font-weight: 600; color: #333; display: block; margin-top: 5px; }
/* GRAPH */
.graph-container { background: #fff; border: 1px solid #ccd0d4; margin-bottom: 15px; padding: 15px; position: relative; }
.graph-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.graph-title-area { display: flex; align-items: center; gap: 8px; }
.graph-title { font-size: 14px; font-weight: 600; color: #1d2327; }
.btn-graph-refresh { cursor: pointer; color: #888; font-size: 16px; display: flex; align-items: center; justify-content: center; width: 20px; height: 20px; border-radius: 50%; transition: all 0.3s; }
.btn-graph-refresh:hover { background: #f0f0f1; color: #2271b1; }
.btn-graph-refresh.spinning { animation: spin 1s linear infinite; color: #2271b1; }
@keyframes spin { 100% { transform: rotate(360deg); } }
.graph-legend { font-size: 11px; color: #2271b1; font-weight: 600; display: flex; align-items: center; gap: 5px; }
.graph-legend span { display: inline-block; width: 12px; height: 2px; background: #2271b1; }
.chart-canvas-box { position: relative; height: 250px; width: 100%; }
.graph-actions { display: flex; gap: 15px; padding-top: 10px; border-top: 1px solid #eee; margin-top: 5px; justify-content: flex-start; }
.action-icon { text-decoration: none; color: #2e8b57; font-size: 18px; opacity: 0.7; transition: opacity 0.2s; }
.action-icon:hover { opacity: 1; color: #1e7040; }
/* LOG */
.log-container { border: 1px solid #ccd0d4; background: #fff; }
.log-header { background: #fcfcfc; padding: 8px 12px; border-bottom: 1px solid #e5e5e5; font-weight: 600; font-size: 13px; }
.summary-table { width: 100%; font-size: 12px; border-collapse: collapse; border-bottom: 1px solid #eee; }
.summary-table td { padding: 6px 12px; border-bottom: 1px solid #f6f6f6; text-align: right; color: #555; }
.summary-table td:first-child { text-align: left; font-weight: 500; }
.visitor-row { padding: 10px 12px; border-bottom: 1px solid #f0f0f1; font-size: 12px; }
.visitor-row:hover { background: #f8f9fa; }
.v-line { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; position: relative; }
.v-icons img { height: 14px; vertical-align: middle; margin-right: 3px; }
.v-ref { color: #666; margin-left: 2px; }
.v-actions { margin-top: 4px; margin-left: 2px; color: #888; display: flex; flex-wrap: wrap; gap: 4px; }
.error-box { color: red; padding: 20px; text-align: center; display: none; }
/* TOOLTIPS */
.m-tooltip-box { visibility: hidden; background-color: #000; color: #fff; text-align: left; border-radius: 4px; padding: 10px; position: absolute; z-index: 99999; bottom: 135%; opacity: 0; transition: opacity 0.2s; font-size: 11px; line-height: 1.4; box-shadow: 0 4px 12px rgba(0,0,0,0.5); pointer-events: none; white-space: normal; }
.m-tooltip-box::after { content: ""; position: absolute; top: 100%; left: 20px; margin-left: -5px; border-width: 5px; border-style: solid; border-color: #000 transparent transparent transparent; }
.flag-wrapper { position: relative; display: inline-block; cursor: help; }
.flag-wrapper .m-tooltip-box { width: 280px; left: 50%; margin-left: -20px; }
.flag-wrapper:hover .m-tooltip-box { visibility: visible; opacity: 1; }
.ft-row { display: block; margin-bottom: 2px; }
.ft-label { color: #aaa; font-weight: 400; margin-right: 4px; }
.ft-val { color: #fff; font-weight: 600; }
.action-wrapper { position: relative; display: inline-block; margin-right: 5px; text-decoration: none; }
.action-wrapper .m-tooltip-box { width: 320px; left: -10px; bottom: 140%; }
.action-wrapper:hover .m-tooltip-box { visibility: visible; opacity: 1; }
.at-url { color: #9ec2e6; font-weight: 600; word-break: break-all; margin-bottom: 4px; font-family: monospace; font-size: 11px; }
.at-title { color: #fff; font-weight: 600; margin-bottom: 4px; font-size: 12px; display: block; }
.at-meta { color: #ccc; display: block; margin-bottom: 2px; }
.profile-link { color: #b0b0b0; text-decoration: none; cursor: pointer; margin-left: 2px; }
.profile-link:hover { color: #2271b1; }
/* ICONOS DASHICONS UNIFICADOS */
.action-wrapper .dashicons { font-size: 18px; color: #999; transition: color 0.2s; }
.action-wrapper:hover .dashicons { color: #2271b1; cursor: pointer; }
</style>
<div class="status-bar"><span class="loading-dot" id="m-dot"></span> <span id="m-status">Conectando...</span></div>
<div class="matomo-js-wrapper">
<div id="m-error" class="error-box"></div>
<div class="hero-box">
<span class="hero-num" id="val-live">--</span>
<div class="hero-desc"><span id="val-live-vis">--</span> visitas y <span id="val-live-act">--</span> acciones (últimos 3 min)</div>
</div>
<div class="kpi-grid">
<div class="kpi-card"><h4>Visitas (Hoy)</h4><span class="val" id="val-uniq">--</span></div>
<div class="kpi-card"><h4>Acciones (Hoy)</h4><span class="val" id="val-page">--</span></div>
</div>
<div class="graph-container">
<div class="graph-header">
<div class="graph-title-area">
<span class="graph-title">Gráfica de las últimas visitas</span>
<span id="btn-refresh-graph" class="btn-graph-refresh dashicons dashicons-update" title="Actualizar Gráfica"></span>
</div>
<div class="graph-legend"><span></span> Visitas (Sesiones)</div>
</div>
<div class="chart-canvas-box">
<canvas id="matomoChart"></canvas>
</div>
<div class="graph-actions">
<span style="font-size:11px; color:#666; padding-top:2px;">Últimos 30 días</span>
<a href="<?php echo esc_url($export_csv); ?>" target="_blank" class="action-icon dashicons dashicons-media-spreadsheet" title="Exportar CSV"></a>
<a href="<?php echo esc_url($export_img); ?>" target="_blank" class="action-icon dashicons dashicons-format-image" title="Descargar Imagen"></a>
</div>
</div>
<div class="log-container">
<div class="log-header">Log de Visitas</div>
<table class="summary-table">
<tr><td>Últimas 24 horas</td><td id="val-24-v">--</td><td id="val-24-a">--</td></tr>
<tr><td>Últimos 30 minutos</td><td id="val-30-v">--</td><td id="val-30-a">--</td></tr>
</table>
<div class="log-rows" id="log-content">
<div style="padding:20px; text-align:center; color:#ccc;">Cargando...</div>
</div>
</div>
</div>
<script>
(function() {
const API_URL = "<?php echo $matomo_url; ?>";
const SITE_ID = "<?php echo $id_site; ?>";
const TOKEN = "<?php echo $token_auth; ?>";
const getEl = (id) => document.getElementById(id);
const buildUrl = (m, e = '') => `${API_URL}index.php?module=API&method=${m}&idSite=${SITE_ID}&format=JSON&token_auth=${TOKEN}${e}&random=${Date.now()}`;
const valOrUnk = (val) => (val && val !== '' && val !== '-' && val !== null) ? val : 'Desconocido';
function getMinutesSinceMidnight() {
const now = new Date();
const midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0);
return Math.max(1, Math.floor((now - midnight) / 60000));
}
function getLocalDateStr() {
const d = new Date();
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
// --- GRÁFICA ---
let myChart = null;
async function fetchGraphData() {
const btn = getEl('btn-refresh-graph');
const ctx = document.getElementById('matomoChart');
if(!ctx) return;
btn.classList.add('spinning');
try {
const resHistory = await fetch(buildUrl('VisitsSummary.get', '&period=day&date=last30')).then(r => r.json());
const minsToday = getMinutesSinceMidnight();
const resLiveToday = await fetch(buildUrl('Live.getCounters', '&lastMinutes=' + minsToday)).then(r => r.json());
if(resHistory.result === 'error') throw new Error('Error Gráfica');
const labels = [];
const dataPoints = [];
const todayStr = getLocalDateStr();
const liveVal = (resLiveToday && resLiveToday[0] && resLiveToday[0].visits) ? resLiveToday[0].visits : 0;
for (const [dateStr, metrics] of Object.entries(resHistory)) {
const d = new Date(dateStr);
const prettyDate = d.toLocaleDateString('es-ES', {weekday:'short', day:'numeric', month:'short'});
labels.push(prettyDate);
if (dateStr === todayStr) dataPoints.push(liveVal);
else dataPoints.push((metrics && metrics.nb_visits) ? metrics.nb_visits : 0);
}
if (myChart) myChart.destroy();
myChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'Visitas',
data: dataPoints,
borderColor: '#2271b1',
backgroundColor: '#2271b1',
borderWidth: 2,
pointRadius: 3,
pointHoverRadius: 5,
pointBackgroundColor: '#2271b1',
tension: 0,
fill: false
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.9)',
titleColor: '#fff',
bodyColor: '#fff',
titleFont: { size: 13, weight: 'bold' },
bodyFont: { size: 13 },
displayColors: true,
boxWidth: 8,
boxHeight: 8,
callbacks: { label: (c) => c.parsed.y + ' Visitas' }
}
},
scales: {
y: { beginAtZero: true, grid: { color: '#f0f0f1' }, ticks: { font: { size: 10 }, color: '#666' } },
x: { grid: { display: false }, ticks: { font: { size: 10 }, color: '#666', maxTicksLimit: 10 } }
}
}
});
} catch(e) { console.log("Error: " + e); } finally { btn.classList.remove('spinning'); }
}
// --- REAL TIME ---
async function fetchRealTimeData() {
const dot = getEl('m-dot');
const status = getEl('m-status');
try {
dot.classList.add('active');
status.innerText = 'Actualizando...';
const minsToday = getMinutesSinceMidnight();
const [res3m, res30m, resTodayLive, res24h, resLog] = await Promise.all([
fetch(buildUrl('Live.getCounters', '&lastMinutes=3')).then(r => r.json()),
fetch(buildUrl('Live.getCounters', '&lastMinutes=30')).then(r => r.json()),
fetch(buildUrl('Live.getCounters', '&lastMinutes=' + minsToday)).then(r => r.json()),
fetch(buildUrl('Live.getCounters', '&lastMinutes=1440')).then(r => r.json()),
fetch(buildUrl('Live.getLastVisitsDetails', '&filter_limit=10')).then(r => r.json())
]);
if(res3m.result === 'error') throw new Error(res3m.message);
const v3m = res3m[0]?.visits || 0;
getEl('val-live').innerText = v3m;
getEl('val-live-vis').innerText = v3m;
getEl('val-live-act').innerText = res3m[0]?.actions || 0;
getEl('val-uniq').innerText = resTodayLive[0]?.visits || 0;
getEl('val-page').innerText = resTodayLive[0]?.actions || 0;
getEl('val-24-v').innerText = (res24h[0]?.visits || 0) + ' Visitas';
getEl('val-24-a').innerText = (res24h[0]?.actions || 0) + ' Acciones';
getEl('val-30-v').innerText = (res30m[0]?.visits || 0) + ' Visitas';
getEl('val-30-a').innerText = (res30m[0]?.actions || 0) + ' Acciones';
let html = '';
if(resLog && resLog.length > 0) {
resLog.forEach(v => {
const fixUrl = (u) => u.startsWith('http') ? u : API_URL + u;
const tCountry = valOrUnk(v.countryPretty || v.country);
const tRegion = valOrUnk(v.region);
const tCity = valOrUnk(v.city);
const tIP = valOrUnk(v.visitIp);
const tID = valOrUnk(v.visitorId);
let rawLang = v.browserLanguage || v.languageCode;
let tLang = (rawLang && rawLang !== '-') ? 'Código de idioma ' + rawLang : 'Desconocido';
const flagTooltipHtml = `<div class="m-tooltip-box"><span class="ft-row"><span class="ft-label">País:</span> <span class="ft-val">${tCountry}</span></span><span class="ft-row"><span class="ft-label">Región:</span> <span class="ft-val">${tRegion}</span></span><span class="ft-row"><span class="ft-label">Ciudad:</span> <span class="ft-val">${tCity}</span></span><span class="ft-row"><span class="ft-label">Idioma:</span> <span class="ft-val">${tLang}</span></span><span class="ft-row"><span class="ft-label">IP:</span> <span class="ft-val">${tIP}</span></span><span class="ft-row"><span class="ft-label">ID:</span> <span class="ft-val">${tID}</span></span></div>`;
const flagImg = v.countryFlag ? `<img src="${fixUrl(v.countryFlag)}">` : '';
const flag = flagImg ? `<div class="flag-wrapper">${flagImg}${flagTooltipHtml}</div>` : '';
const date = new Date(v.serverTimestamp * 1000);
const timeStr = date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit', second:'2-digit'});
const dateStr = date.toLocaleDateString([], {weekday: 'short', day: 'numeric'});
const browser = v.browserIcon ? `<img src="${fixUrl(v.browserIcon)}" title="${v.browserName}">` : '';
const os = v.operatingSystemIcon ? `<img src="${fixUrl(v.operatingSystemIcon)}" title="${v.operatingSystemName}">` : '';
const profileUrl = `${API_URL}index.php?module=CoreHome&action=index&idSite=${SITE_ID}&period=day&date=today#?idSite=${SITE_ID}&period=day&date=today&category=Dashboard_Dashboard&subcategory=1&popover=visitorProfile%243A${v.visitorId}`;
let refHtml = 'Entrada directa';
if(v.referrerTypeName === 'Motores de búsqueda') {
refHtml = `<span style="color:#d63638;font-weight:bold">G</span> ${v.referrerName || ''}`;
if(v.referrerKeyword) refHtml += ` <i>"${v.referrerKeyword}"</i>`;
} else if(v.referrerTypeName === 'Sitios web') {
refHtml = `Ref: <a href="${v.referrerUrl}" target="_blank" style="color:#666">${v.referrerName}</a>`;
}
let actHtml = '';
if(v.actionDetails) {
v.actionDetails.forEach(a => {
if(a.type) {
let dashiconClass = 'dashicons-portfolio'; // Default
let isEvent = false;
let isSearch = false;
switch(a.type) {
case 'download': dashiconClass = 'dashicons-download'; break;
case 'outlink': dashiconClass = 'dashicons-external'; break;
case 'event': dashiconClass = 'dashicons-megaphone'; isEvent = true; break;
case 'search':
case 'siteSearch':
dashiconClass = 'dashicons-search';
isSearch = true;
break;
}
const pUrl = a.url || '#';
let actionDateStr = '';
if(a.timestamp) {
const ad = new Date(a.timestamp * 1000);
actionDateStr = ad.toLocaleDateString('es-ES', {day: 'numeric', month: 'short', year: 'numeric'}) + ' ' + ad.toLocaleTimeString();
}
let timeSpent = '0s';
if(a.timeSpent) {
const m = Math.floor(a.timeSpent / 60);
const s = a.timeSpent % 60;
timeSpent = (m > 0 ? m + 'm ' : '') + s + 's';
}
// CONSTRUCCIÓN TOOLTIP
let tooltipInner = '';
if (isEvent) {
let evText = `Evento ${a.eventCategory || ''} - ${a.eventAction || ''}`;
if(a.eventName) evText += ` - ${a.eventName}`;
if(a.pageTitle) evText += ` - ${a.pageTitle}`;
const evValue = (a.eventValue !== undefined) ? a.eventValue : '0';
evText += ` - ${evValue}`;
tooltipInner = `<div class="at-title">${evText}</div><div class="at-meta">${actionDateStr}</div>`;
} else if (isSearch) {
// Lógica Búsqueda Interna
const keyword = a.siteSearchKeyword || a.actionName || 'Sin palabras clave';
tooltipInner = `<div class="at-title">Búsqueda interna: ${keyword}</div><div class="at-meta">${actionDateStr}</div>`;
} else {
// Lógica Estándar
const pTitle = (a.pageTitle || 'Título desconocido').replace(/"/g, '"');
tooltipInner = `<div class="at-url">${pUrl}</div><div class="at-title">${pTitle}</div><div class="at-meta">${actionDateStr}</div><div class="at-meta">Tiempo en la página: ${timeSpent}</div>`;
}
actHtml += `<a href="${pUrl}" target="_blank" class="action-wrapper"><span class="dashicons ${dashiconClass}"></span><div class="m-tooltip-box">${tooltipInner}</div></a>`;
}
});
}
html += `
<div class="visitor-row">
<div class="v-line">
<b>${timeStr}</b> <span style="font-size:11px;color:#888">(${dateStr})</span>
<span class="v-icons" style="display:flex;align-items:center;gap:3px;">${flag}${browser}${os}</span>
<a href="${profileUrl}" target="_blank" class="profile-link dashicons dashicons-businessperson" title="Ver Perfil"></a>
</div>
<div class="v-line v-ref">${refHtml}</div>
<div class="v-line v-actions">${actHtml}</div>
</div>`;
});
} else {
html = '<div style="padding:20px;text-align:center;color:#999">Sin visitas recientes.</div>';
}
getEl('log-content').innerHTML = html;
status.innerText = 'En vivo (Actualizado: ' + new Date().toLocaleTimeString() + ')';
} catch (err) {
getEl('m-error').style.display = 'block';
getEl('m-error').innerText = 'Error: ' + err.message;
status.innerText = 'Error';
} finally {
dot.classList.remove('active');
}
}
fetchGraphData();
fetchRealTimeData();
setInterval(fetchRealTimeData, 10000);
getEl('btn-refresh-graph').addEventListener('click', function() { fetchGraphData(); });
})();
</script>
<?php
}
}4. ملخص الميزات
بنية جافا سكريبت 100٪ (من جانب العميل): على عكس الأدوات الذكية التقليدية التي تستخدم PHP(cURL) لطلب البيانات، تستخدم هذه الأداة الذكية جافا سكريبت (الزحف) مباشرة من متصفحك .
- ميزة؟ خادم ووردبريس الخاص بك معطل. لا توجد أقفال أمان للاتصال بنفسك (Loopback) ولا توجد أوقات تحميل بطيئة في المشرف. التحميل غير متزامن.
"التنظيف" في الوقت الحقيقي (التحديث التلقائي): تقوم الأداة تلقائيًا بتحديث العدادات وسجل الزوار كل 10 ثوانٍ . يمكنك ترك علامة التبويب مفتوحة ومشاهدة الزيارات الواردة والعدادات ترتفع دون الحاجة إلى لمس أي شيء.
الرسومات الهجينة الذكية: إليك الحيلة التقنية الأكثر إثارة للاهتمام. يقوم Matomo عادةً "بأرشفة" البيانات كل ساعة، لذا فإن الرسم البياني اليوم غالبًا ما يكون قديمًا أو لاغياً. تجمع هذه الأداة بين مصدرين للبيانات:
النتيجة: رسم بياني لمدة 30 يومًا حيث تكون نقطة "اليوم" حقيقية ودقيقة. بالإضافة إلى أنه يحتوي على زر تحديث يدوي منفصل حتى لا يتم الخلط بين واجهة برمجة التطبيقات دون داعٍ.
بيانات آخر 29 يومًا: بيانات من الأرشيف التاريخي (تحميل سريع).
اليوم: يتم حساب ذلك في الوقت الفعلي عن طريق طلب واجهة برمجة التطبيقات (API) للدقائق المنقضية بين منتصف الليل والثانية الحالية.






