Miten luoda reaaliaikainen kävijä Matomo widget WordPress työpöydälle (ilman plugins)

Ei kommentteja

Seleccionar idioma

Miten luoda reaaliaikainen kävijä Matomo widget WordPress työpöydälle (ilman plugins) 1

On kulunut yli kaksi vuotta siitä, kun lopetin Google Analyticsin käytön ja siirryin Matomo On-Premise -palveluun (itse isännöity). Ja niin onnellinen.

Jos käytät Matomo On-Premiseä, tiedät jo, että se on tehokas ja täydellinen työkalu, mutta uuden välilehden avaaminen joka kerta, kun haluat nähdä perustilastot, on epämiellyttävää ja ehkä jopa liian vaativaa palvelimellesi, vaikka sen suorituskyky on parantunut paljon sen alkuajoista, jolloin sen nimi oli Piwic, joka söi palvelimen hetkessä pyynnöillä.

Matomo-integraatiolisäkkeitä on olemassa, mutta ne kärsivät yleensä kahdesta ongelmasta: joko ne näyttävät iframe-kehyksiä, jotka on estetty turvallisuussyistä, tai ne tekevät synkronisia PHP-pyyntöjä, jotka hidastavat hallintapaneelia.

Matomo API:sta vedettävä Widget

Lähdin luomaan WordPressin työpöydälle Matomo API:n avulla natiivia widgettiä, joka olisi kevyt, esteettinen ja ennen kaikkea reaaliaikainen, mutta jolla ei olisi mitään vaikutusta palvelimeen ja joka toimisi asiakkaan selaimessa. Tässä tukeuduin Matomon dokumentaatioon ja Gemini 3.0:n apuun, joka on hiljattain julkaistussa versiossaan parantunut paljon koodin käsittelyn osalta ja koska en osaa kirjoittaa yli kolmea riviä rikkomatta jotain.

Varoitus: widget on suunniteltu On-Premise-asennuksille (sama palvelin tai oma palvelin), vaikka se on JS, se toimisi myös, jos sinulla on Matomo toisessa verkkotunnuksessa, kunhan CORS (selaimen oikeudet) sallii sen.

Sen toimintojen selittämisen ja lisäämisen jälkeen löydät täydellisen koodin, jonka voit lisätä mallisi, lapsiteemasi tai snippets-lisäosan functions.php-tiedostoon ja sisällyttää siihen URL-osoitteesi, projektisi ID:n ja Matomo Auth Tokenin.

Reaaliaikaiset perustiedot

Koska en tarvitse paljon mittareita widgetissä, asetin siihen kolme perusruutua, jotka tarkastan rutiininomaisesti.

Ensimmäinen on online-kävijälaskuri, joka päivittyy automaattisesti kymmenen sekunnin välein.

Alla on kaksi laatikkoa, joissa on päivän käyntien kokonaismäärä ja oikealla "Toiminnot"-laatikko, myös päivän osalta, joka, vaikka Matomo kutsuukin sitä "näyttökerroiksi", ei ole oikeastaan aivan tarkka, koska se lisää myös muut toiminnot, kuten kuvan avaamisen, haun tekemisen, klikkaustapahtuman käynnistämisen tai ulkoiseen URL-osoitteeseen johtavan linkin klikkaamisen. Molemmat laatikot käyttävät 30 päivän historiallisia tietoja sekä reaaliaikaista datainjektiota.

Miten luoda reaaliaikainen kävijä Matomo widget WordPress työpöydälle (ilman plugins) 2

Käyntikertojen kuvaaja on ainoa ruutu, jota ei päivitetä reaaliajassa, koska ei ole järkevää pakottaa tarpeettomia pyyntöjä tietoihin, jotka meillä on jo "Käynnit (tänään)" -laatikossa, ja päivityspainike on lisätty, jotta kuvaaja voidaan päivittää milloin tahansa vain kerran. Alla on linkki, josta voit ladata CSV-tiedoston tai kuvan kuvaajasta viimeisten 30 päivän ajalta.

Tässä kaaviossa käytämme Chart.js-kirjastoa, jonka avulla kaavio voidaan piirtää staattisen kuvan sijasta, mikä mahdollistaa vuorovaikutuksen ja mahdollistaa päiväkohtaisten tietojen näkymisen pisteissä.

Miten luoda reaaliaikainen kävijä Matomo widget WordPress työpöydälle (ilman plugins) 3

Neljännessä laatikossa on yksityiskohtainen kävijäloki, jossa näkyvät bannerit, selaimet, käyttöjärjestelmät ja linkki natiiviin kävijäprofiiliraporttiin. Kaikki toiminnot esitetään visuaalisesti vastaavilla kuvakkeilla, jotka kuvaavat vierailuja, latauksia (kuvia, pdf-tiedostoja tai mitä tahansa), ulkoisten linkkien klikkauksia, sivuston sisäisiä hakuja ja tapahtumia.

Miten luoda reaaliaikainen kävijä Matomo widget WordPress työpöydälle (ilman plugins) 4

Kuvakkeilla on"rikkaat" työkaluvihjeet mustien laatikoiden muodossa, jotka jäljittelevät Matomon tyyliä ja sisältävät kaikki olennaiset vähimmäistiedot.

Miten luoda reaaliaikainen kävijä Matomo widget WordPress työpöydälle (ilman plugins) 5

Miten luoda reaaliaikainen kävijä Matomo widget WordPress työpöydälle (ilman plugins) 6

Toteutettavat toimet

1. Hanki Matomo-tunnuksesi

Tarvitset "avaimen", jotta WordPress voi "kommunikoida" Matomon kanssa:

  1. Syötä Matomo.
  2. Siirry kohtaan Hallinto (vaihde) > Henkilöstö > Turvallisuus.
  3. Luo uusi Auth Token. Kopioi tämä merkkijono. Kirjoita se heti muistiin ja tallenna se, koska se näkyy vain kerran ja jos kadotat sen, sinun on luotava uusi.

Varmista, että nämä kaksi näkyviin tulevaa ruutua EIVÄT ole valittuna, ja jos ne ovat, poista niiden valintamerkki, jotta ne näkyvät kuvan mukaisesti:

Miten luoda reaaliaikainen kävijä Matomo widget WordPress työpöydälle (ilman plugins) 7

2. Konfigurointi

Kun olet lisännyt koodin functions.php:hen, muokkaa vain näitä kolmea riviä // --- CONFIGURATION --- - kohtaan lisätäksesi tietosi:

$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. Koodi

/**
 * 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. Yhteenveto ominaisuuksista


100 % JavaScript-arkkitehtuuri (Client-Side): Toisin kuin perinteiset widgetit, jotka käyttävät PHP:tä(cURL) tietojen pyytämiseen, tämä widget käyttää JavaScriptiä (nouto) suoraan selaimesta.

  • Etu? WordPress-palvelimesi ei toimi. Itsensä kutsumiseen ei ole turvasulkuja (Loopback) eikä hitaita latausaikoja ylläpidossa. Lataus on asynkronista.

Reaaliaikainen "puhdas" (automaattinen päivitys): Widget päivittää laskurit ja kävijälokin automaattisesti 10 sekunnin välein. Voit jättää välilehden auki ja katsella, kuinka vierailuja tulee ja laskurit nousevat ilman, että kosketat mitään.

Älykäs hybridigrafiikka: Tässä on mielenkiintoisin tekninen temppu. Matomo "arkistoi" tiedot yleensä tunnin välein, joten tämän päivän kaavio on usein vanhentunut tai nolla. Tämä widget yhdistää kaksi tietolähdettä:

Tulos: 30 päivän kaavio, jossa "Tänään"-kohta on todellinen ja tarkka. Lisäksi siinä on erillinen manuaalinen päivityspainike, jotta API ei sotkeudu tarpeettomasti.

Viimeisten 29 päivän tiedot: Historiallisen arkiston tiedot (nopea lataus).

Tänään: Tämä lasketaan reaaliaikaisesti kysymällä API:lta minuutteja, jotka ovat kuluneet keskiyöstä kuluvaan sekuntiin.

Aiheeseen liittyvät artikkelit

Jätä kommentti

Este blog se aloja en LucusHost

LucusHost, el mejor hosting