Πώς να δημιουργήσετε ένα widget Matomo επισκέπτη σε πραγματικό χρόνο για την επιφάνεια εργασίας WordPress (χωρίς plugins)

No comments

Seleccionar idioma

Πώς να δημιουργήσετε ένα widget Matomo επισκέπτη σε πραγματικό χρόνο για την επιφάνεια εργασίας WordPress (χωρίς plugins) 1

Έχουν περάσει περισσότερα από δύο χρόνια από τότε που σταμάτησα να χρησιμοποιώ το Google Analytics και προχώρησα στη χρήση του Matomo On-Premise (self-hosted). Και είμαι τόσο χαρούμενος.

Αν χρησιμοποιείτε το Matomo On-Premise, γνωρίζετε ήδη ότι είναι ένα ισχυρό και πλήρες εργαλείο, αλλά το να ανοίγετε μια νέα καρτέλα κάθε φορά που θέλετε να δείτε τα βασικά στατιστικά στοιχεία είναι άβολο και ίσως και πολύ απαιτητικό για τον διακομιστή σας, αν και έχει βελτιωθεί πολύ η απόδοσή του από τις αρχές του, όταν ονομαζόταν Piwic, το οποίο έτρωγε τον διακομιστή με το καλημέρα με αιτήσεις.

Υπάρχουν πρόσθετα ενσωμάτωσης του Matomo, αλλά τείνουν να υποφέρουν από δύο προβλήματα: είτε εμφανίζουν iframes που είναι μπλοκαρισμένα για λόγους ασφαλείας, είτε κάνουν σύγχρονες αιτήσεις PHP που επιβραδύνουν τον πίνακα διαχείρισης.

Widget που αντλεί από το Matomo API

Ξεκίνησα να δημιουργήσω ένα εγγενές widget για την επιφάνεια εργασίας του WordPress χρησιμοποιώντας το Matomo API, το οποίο θα ήταν ελαφρύ, αισθητικό και, πάνω απ' όλα, σε πραγματικό χρόνο, αλλά θα είχε μηδενική επίδραση στον διακομιστή και όλα θα εκτελούνταν στο πρόγραμμα περιήγησης του πελάτη. Για αυτό βασίστηκα στην τεκμηρίωση του Matomo και στη βοήθεια του Gemini 3.0, το οποίο έχει βελτιωθεί πολύ στην έκδοση που κυκλοφόρησε πρόσφατα όσον αφορά την επεξεργασία του κώδικα και επειδή δεν ξέρω πώς να γράψω πάνω από τρεις γραμμές χωρίς να σπάσω κάτι.

Προειδοποίηση: το widget έχει σχεδιαστεί για On-Premise εγκαταστάσεις (ίδιος διακομιστής ή δικός σας διακομιστής), αν και καθώς είναι JS θα μπορούσε επίσης να λειτουργήσει αν έχετε το Matomo σε άλλο τομέα, εφόσον το επιτρέπει το CORS (δικαιώματα του προγράμματος περιήγησης).

Μετά την επεξήγηση των λειτουργιών του και τον τρόπο προσθήκης του, θα βρείτε τον πλήρη κώδικα μόνο για να τον προσθέσετε στο functions.php του προτύπου σας, του παιδικού σας θέματος ή με το snippets plugin και να συμπεριλάβετε τη διεύθυνση URL σας, το ID του έργου σας και το Matomo Auth Token.

Βασικά δεδομένα σε πραγματικό χρόνο

Καθώς δεν χρειάζομαι πολλές μετρήσεις στο widget, το έχω ρυθμίσει με τα τρία βασικά κουτάκια που ελέγχω συνήθως.

Η πρώτη είναι ο μετρητής επισκεπτών στο διαδίκτυο, ο οποίος ενημερώνεται αυτόματα κάθε δέκα δευτερόλεπτα.

Παρακάτω υπάρχουν δύο πλαίσια, με το συνολικό αριθμό επισκέψεων για την ημέρα και στα δεξιά το πλαίσιο "Ενέργειες", επίσης για την ημέρα, το οποίο αν και το Matomo το ονομάζει "Εντυπώσεις" δεν είναι στην πραγματικότητα αρκετά ακριβές, επειδή προσθέτει και άλλες ενέργειες, όπως το άνοιγμα μιας εικόνας, η πραγματοποίηση μιας αναζήτησης, η ενεργοποίηση ενός συμβάντος κλικ ή το κλικ σε έναν σύνδεσμο προς μια εξωτερική διεύθυνση URL. Και τα δύο πλαίσια τροφοδοτούνται από ιστορικά δεδομένα 30 ημερών καθώς και από έγχυση δεδομένων σε πραγματικό χρόνο.

Πώς να δημιουργήσετε ένα widget Matomo επισκέπτη σε πραγματικό χρόνο για την επιφάνεια εργασίας WordPress (χωρίς plugins) 2

Το γράφημα των επισκέψεων είναι το μόνο πλαίσιο που δεν ενημερώνεται σε πραγματικό χρόνο, επειδή δεν έχει νόημα να επιβάλλουμε περιττές αιτήσεις για δεδομένα που έχουμε ήδη στο πλαίσιο "Επισκέψεις (σήμερα)" και προστίθεται ένα κουμπί ανανέωσης για την ανανέωση του γραφήματος ανά πάσα στιγμή μόνο μία φορά. Παρακάτω υπάρχει ένας σύνδεσμος για να κατεβάσετε ένα CSV ή μια εικόνα του γραφήματος για τις τελευταίες 30 ημέρες.

Για αυτό το διάγραμμα χρησιμοποιούμε τη βιβλιοθήκη Chart.js, η οποία επιτρέπει τη σχεδίαση του διαγράμματος αντί της χρήσης μιας στατικής εικόνας, επιτρέποντας την αλληλεπίδραση και τη δυνατότητα να βλέπουμε τα δεδομένα ανά ημέρα στα σημεία.

Πώς να δημιουργήσετε ένα widget Matomo επισκέπτη σε πραγματικό χρόνο για την επιφάνεια εργασίας WordPress (χωρίς plugins) 3

Το τέταρτο πλαίσιο είναι το λεπτομερές αρχείο καταγραφής επισκεπτών που δείχνει: banners, προγράμματα περιήγησης, λειτουργικά συστήματα και έναν σύνδεσμο προς την αναφορά του προφίλ του επισκέπτη. Όλες οι ενέργειες απεικονίζονται οπτικά με αντίστοιχα εικονίδια για επισκέψεις, λήψεις (εικόνων ή pdf ή οτιδήποτε άλλο), κλικ σε εξωτερικούς συνδέσμους, εσωτερικές αναζητήσεις στον ιστότοπο και συμβάντα.

Πώς να δημιουργήσετε ένα widget Matomo επισκέπτη σε πραγματικό χρόνο για την επιφάνεια εργασίας WordPress (χωρίς plugins) 4

Τα εικονίδια έχουν"πλούσια" tooltips με τη μορφή μαύρων κουτιών που μιμούνται το εγγενές στυλ του Matomo με όλες τις ελάχιστες σχετικές πληροφορίες.

Πώς να δημιουργήσετε ένα widget Matomo επισκέπτη σε πραγματικό χρόνο για την επιφάνεια εργασίας WordPress (χωρίς plugins) 5

Πώς να δημιουργήσετε ένα widget Matomo επισκέπτη σε πραγματικό χρόνο για την επιφάνεια εργασίας WordPress (χωρίς plugins) 6

Βήματα που πρέπει να ακολουθήσετε

1. Αποκτήστε το κουπόνι Matomo

Χρειάζεστε ένα "κλειδί" ώστε το WordPress σας να μπορεί να "επικοινωνεί" με το Matomo σας:

  1. Εισάγετε το Matomo σας.
  2. Πηγαίνετε στη Διαχείριση (Gear) > Προσωπικό > Ασφάλεια.
  3. Δημιουργήστε ένα νέο Token Auth. Αντιγράψτε αυτή τη συμβολοσειρά χαρακτήρων. Σημειώστε το αμέσως και αποθηκεύστε το, επειδή θα το εμφανίσει μόνο μία φορά και αν το χάσετε θα πρέπει να δημιουργήσετε ένα νέο.

Πρέπει να βεβαιωθείτε ότι αυτά τα δύο κουτάκια που εμφανίζονται ΔΕΝ είναι τσεκαρισμένα, και αν είναι, καταργήστε το τσεκάρισμά τους ώστε να εμφανίζονται όπως στην εικόνα:

Πώς να δημιουργήσετε ένα widget Matomo επισκέπτη σε πραγματικό χρόνο για την επιφάνεια εργασίας WordPress (χωρίς plugins) 7

2. Διαμόρφωση

Αφού προσθέσετε τον κώδικα στο functions.php, απλά επεξεργαστείτε αυτές τις τρεις γραμμές στο // --- CONFIGURATION --- για να προσθέσετε τα δεδομένα σας:

$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, '&quot;');
                                            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% αρχιτεκτονική JavaScript (Client-Side): Σε αντίθεση με τα παραδοσιακά widgets που χρησιμοποιούν PHP(cURL) για να ζητούν δεδομένα, αυτό το widget χρησιμοποιεί JavaScript (fetch) απευθείας από το πρόγραμμα περιήγησής σας.

  • Το πλεονέκτημα; Ο διακομιστής σας WordPress δεν λειτουργεί. Δεν υπάρχουν κλειδαριές ασφαλείας για την κλήση του ίδιου (Loopback) και δεν υπάρχουν αργοί χρόνοι φόρτωσης στο admin. Η φόρτωση είναι ασύγχρονη.

Καθαρός" πραγματικός χρόνος (Αυτόματη ανανέωση): Το widget ανανεώνει αυτόματα τους μετρητές και το αρχείο καταγραφής επισκεπτών κάθε 10 δευτερόλεπτα. Μπορείτε να αφήσετε την καρτέλα ανοιχτή και να παρακολουθείτε τις επισκέψεις που έρχονται και τους μετρητές να ανεβαίνουν χωρίς να αγγίζετε τίποτα.

Έξυπνα υβριδικά γραφικά: Εδώ είναι το πιο ενδιαφέρον τεχνικό τέχνασμα. Το Matomo συνήθως "αρχειοθετεί" τα δεδομένα κάθε ώρα, οπότε το σημερινό γράφημα είναι συχνά ξεπερασμένο ή μηδενικό. Αυτό το widget συνδυάζει δύο πηγές δεδομένων:

Αποτέλεσμα: Ένα διάγραμμα 30 ημερών όπου το σημείο "Σήμερα" είναι πραγματικό και ακριβές. Επιπλέον, διαθέτει ξεχωριστό κουμπί χειροκίνητης ανανέωσης, ώστε να μην είναι άσκοπα γεμάτο το API.

Δεδομένα για τις τελευταίες 29 ημέρες: Δεδομένα από το ιστορικό αρχείο (γρήγορη φόρτωση).

Σήμερα: Υπολογίζεται σε πραγματικό χρόνο ζητώντας από το API τα λεπτά που έχουν περάσει από τα μεσάνυχτα μέχρι το τρέχον δευτερόλεπτο.

Σχετικά άρθρα

Σχολιάστε

Este blog se aloja en LucusHost

LucusHost, el mejor hosting