diff --git a/CHANGELOG.md b/CHANGELOG.md index f8ddae1..0405f9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ Formato: `X.Y.Z` según `VERSIONADO.md` --- +## [1.2.0] — 2026-06-05 + +### feat: Analytics propio — historial diario, gráfica 30 días, top páginas + +- **Tabla `cv_cv_visits`** creada en BD dev (`.20`) y prod (`.32`): registra visitas únicas por día y URL. +- **Migración automática** del acumulado anterior (`persona/cvolio/lumina/apex_total_visits`) a la nueva tabla. +- **Registro de visitas** en `template_redirect` — deduplicado por IP+URI+hora con transient, solo para visitantes no autenticados. +- **Limpieza semanal** vía WP-Cron: elimina registros con más de 365 días. +- **Widget de dashboard** `📊 CV Analytics — Estadísticas` con: 4 tarjetas (Hoy / 7d / 30d / Total), gráfica de barras CSS últimos 30 días sin librerías externas, top 5 páginas con porcentaje, nota LOPDP al pie. + +--- + ## [1.1.0] — 2026-06-05 ### feat: LOPDP Ecuador completo diff --git a/VERSION b/VERSION index 9084fa2..26aaba0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.0 +1.2.0 diff --git a/backend/wordpress/wp-content/plugins/pflores-cv-fields/pflores-cv-fields.php b/backend/wordpress/wp-content/plugins/pflores-cv-fields/pflores-cv-fields.php index c0f4044..19dac7b 100644 --- a/backend/wordpress/wp-content/plugins/pflores-cv-fields/pflores-cv-fields.php +++ b/backend/wordpress/wp-content/plugins/pflores-cv-fields/pflores-cv-fields.php @@ -459,34 +459,168 @@ function pflores_lopd_banner(): void prefix . 'cv_visits'; + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + dbDelta("CREATE TABLE IF NOT EXISTS `{$table}` ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + fecha DATE NOT NULL, + pagina VARCHAR(255) NOT NULL DEFAULT '/', + visitas INT UNSIGNED NOT NULL DEFAULT 1, + UNIQUE KEY fecha_pagina (fecha, pagina) + ) {$wpdb->get_charset_collate()};"); + $old = array_sum(array_map( + fn ($k) => (int) get_option("{$k}_total_visits", 0), + ['persona', 'cvolio', 'lumina', 'apex'] + )); + if ($old > 0) { + $wpdb->query($wpdb->prepare( + "INSERT IGNORE INTO `{$table}` (fecha,pagina,visitas) VALUES (%s,'/',%d)", + '2026-01-01', + $old + )); + } + update_option('pflores_visits_table_v1', true); +}); + +// C2) Registrar visita (template_redirect) +add_action('template_redirect', function () { + if (is_admin() || wp_doing_ajax() || wp_doing_cron()) { + return; + } + if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'GET') { + return; + } + if (is_user_logged_in() && current_user_can('manage_options')) { + return; + } + if (! is_front_page() && ! is_home() && ! is_singular()) { + return; + } + if (! get_option('pflores_visits_table_v1')) { + return; + } + $uri = strtok(sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'] ?? '/')), '?'); + $key = 'pflores2_' . md5($uri . '|' . pflores_client_ip() . '|' . gmdate('Y-m-d-H')); + if (get_transient($key)) { + return; + } + set_transient($key, '1', HOUR_IN_SECONDS); + global $wpdb; + $wpdb->query($wpdb->prepare( + "INSERT INTO `{$wpdb->prefix}cv_visits` (fecha,pagina,visitas) VALUES (CURDATE(),%s,1) + ON DUPLICATE KEY UPDATE visitas=visitas+1", + $uri + )); +}); + +function pflores_client_ip(): string +{ + foreach (['HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP', 'REMOTE_ADDR'] as $k) { + if (! empty($_SERVER[$k])) { + return trim(explode(',', (string) $_SERVER[$k])[0]); + } + } + return '0.0.0.0'; +} + +// C3) Limpieza semanal (cron) +add_action('init', function () { + if (! wp_next_scheduled('pflores_visits_cleanup')) { + wp_schedule_event(time(), 'weekly', 'pflores_visits_cleanup'); + } +}); +add_action('pflores_visits_cleanup', function () { + global $wpdb; + $wpdb->query("DELETE FROM `{$wpdb->prefix}cv_visits` + WHERE fecha < DATE_SUB(CURDATE(), INTERVAL 365 DAY)"); +}); + +// C4) Widget de Analytics en el dashboard +add_action('wp_dashboard_setup', 'pflores_analytics_widget'); +function pflores_analytics_widget(): void { wp_add_dashboard_widget( - 'pflores_visitas_totales', - 'CV Patricia Flores — Estadísticas de visitas', - 'pflores_render_visitas_widget' + 'pflores_analytics', + '📊 CV Analytics — Estadísticas', + 'pflores_render_analytics_widget' ); } -function pflores_render_visitas_widget(): void + +function pflores_render_analytics_widget(): void { - $temas = [ - 'persona-cv' => (int) get_option('persona_total_visits', 0), - 'cvolio' => (int) get_option('cvolio_total_visits', 0), - 'lumina' => (int) get_option('lumina_total_visits', 0), - 'apex-cv' => (int) get_option('apex_total_visits', 0), + global $wpdb; + $t = $wpdb->prefix . 'cv_visits'; + + $hoy = (int) $wpdb->get_var("SELECT COALESCE(SUM(visitas),0) FROM `{$t}` WHERE fecha=CURDATE()"); + $sem = (int) $wpdb->get_var("SELECT COALESCE(SUM(visitas),0) FROM `{$t}` WHERE fecha>=DATE_SUB(CURDATE(),INTERVAL 7 DAY)"); + $mes = (int) $wpdb->get_var("SELECT COALESCE(SUM(visitas),0) FROM `{$t}` WHERE fecha>=DATE_SUB(CURDATE(),INTERVAL 30 DAY)"); + $total = (int) $wpdb->get_var("SELECT COALESCE(SUM(visitas),0) FROM `{$t}`"); + + $dias30 = $wpdb->get_results( + "SELECT fecha, SUM(visitas) AS v FROM `{$t}` + WHERE fecha>=DATE_SUB(CURDATE(),INTERVAL 30 DAY) + GROUP BY fecha ORDER BY fecha ASC", + ARRAY_A + ) ?: []; + + $top = $wpdb->get_results( + "SELECT pagina, SUM(visitas) AS v FROM `{$t}` + GROUP BY pagina ORDER BY SUM(visitas) DESC LIMIT 5", + ARRAY_A + ) ?: []; + + // Tarjetas resumen + $cards = [ + ['Hoy', $hoy, '#1a7a8a'], + ['7 días', $sem, '#1a7a8a'], + ['30 días', $mes, '#1a7a8a'], + ['Total', $total, '#c0392b'], ]; - $total = array_sum($temas); - echo '
| ' . esc_html($slug) . ' | '; - echo '' . esc_html((string) $visitas) . ' |
| Total | '; - echo '' . esc_html((string) $total) . ' |
Visitas únicas por hora (LOPDP — datos anónimos)
'; + echo ''; + + // Gráfica de barras — últimos 30 días + if (! empty($dias30)) { + $maxV = max(array_column($dias30, 'v')) ?: 1; + echo 'Últimos 30 días
'; + echo 'Top páginas
'; + echo '| ' . esc_html($row['pagina']) . ' | '; + echo '' . esc_html((string) $row['v']) . ' | '; + echo '' . esc_html((string) $pct) . '% | '; + echo '
Visitas únicas/hora · LOPDP — datos anónimos
'; } /* ─────────────────────────────────────────────