Semplifica il grafico analytics: solo pageview e toggle nascondi.

Chiarisce che il totale è su tutte le pagine (filtri Da/A/Device), distinto dal dettaglio per path.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-08 22:07:34 +02:00
co-authored by Cursor
parent 53ad26fdbe
commit 81e7e3c9af
10 changed files with 143 additions and 61 deletions
@@ -58,42 +58,48 @@
<% end %> <% end %>
</div> </div>
<% if @trend.any? && (@trend_totals[:pageviews].positive? || @trend_totals[:clicks].positive? || @trend_totals[:moves].positive?) %> <% if @trend.any? && @trend_totals[:pageviews].positive? %>
<section class="panel admin-analytics-trend"> <section class="panel admin-analytics-trend" id="admin-analytics-trend">
<h3><%= t("admin.analytics.index.trend_title") %></h3> <div class="admin-analytics-trend__head">
<p class="muted admin-table-sub"><%= t("admin.analytics.index.trend_lead") %></p> <div>
<div class="kpi-grid admin-analytics-trend__kpi"> <h3><%= t("admin.analytics.index.trend_title") %></h3>
<div class="kpi"> <p class="muted admin-table-sub"><%= t("admin.analytics.index.trend_lead") %></p>
<div class="kpi-label"><%= t("admin.analytics.index.table.pageviews") %></div>
<div class="kpi-value"><%= @trend_totals[:pageviews] %></div>
</div>
<div class="kpi">
<div class="kpi-label"><%= t("admin.analytics.index.table.clicks") %></div>
<div class="kpi-value"><%= @trend_totals[:clicks] %></div>
</div>
<div class="kpi">
<div class="kpi-label"><%= t("admin.analytics.index.table.moves") %></div>
<div class="kpi-value"><%= @trend_totals[:moves] %></div>
</div> </div>
<button type="button"
id="admin-analytics-trend-toggle"
class="admin-btn admin-btn--outline admin-btn--sm"
aria-controls="admin-analytics-trend-body"
aria-expanded="true"><%= t("admin.analytics.index.hide_chart") %></button>
</div> </div>
<div class="chart-wrap chart-wrap--analytics"> <div id="admin-analytics-trend-body">
<canvas id="chart-analytics-trend" aria-label="<%= t("admin.analytics.index.trend_title") %>"></canvas> <div class="kpi-grid admin-analytics-trend__kpi">
<div class="kpi">
<div class="kpi-label"><%= t("admin.analytics.index.trend_total_label") %></div>
<div class="kpi-value"><%= @trend_totals[:pageviews] %></div>
<div class="kpi-sub muted"><%= t("admin.analytics.index.trend_total_hint", from: l(@filters[:from]), to: l(@filters[:to])) %></div>
</div>
</div>
<div class="chart-wrap chart-wrap--analytics">
<canvas id="chart-analytics-trend" aria-label="<%= t("admin.analytics.index.trend_title") %>"></canvas>
</div>
</div> </div>
</section> </section>
<script> <script>
window.adminAnalyticsTrend = <%= raw @trend.to_json %>; window.adminAnalyticsTrend = <%= raw @trend.to_json %>;
window.adminAnalyticsI18n = { window.adminAnalyticsI18n = {
pageviews: <%= raw t("admin.analytics.index.table.pageviews").to_json %>, pageviews: <%= raw t("admin.analytics.index.table.pageviews").to_json %>,
clicks: <%= raw t("admin.analytics.index.table.clicks").to_json %>, hideChart: <%= raw t("admin.analytics.index.hide_chart").to_json %>,
moves: <%= raw t("admin.analytics.index.table.moves").to_json %> showChart: <%= raw t("admin.analytics.index.show_chart").to_json %>
}; };
</script> </script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js" crossorigin="anonymous"></script>
<script src="/admin-analytics.js?v=1" defer></script> <script src="/admin-analytics.js?v=2" defer></script>
<% end %> <% end %>
<div class="panel"> <div class="panel">
<% if @pages.any? %> <% if @pages.any? %>
<h3 class="admin-analytics-pages-title"><%= t("admin.analytics.index.pages_title") %></h3>
<p class="muted admin-table-sub"><%= t("admin.analytics.index.pages_lead") %></p>
<div class="admin-table-wrap"> <div class="admin-table-wrap">
<table class="admin-table"> <table class="admin-table">
<thead> <thead>
+1 -1
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow"> <meta name="robots" content="noindex, nofollow">
<%= csrf_meta_tags %> <%= csrf_meta_tags %>
<link rel="stylesheet" href="/admin.css?v=17"> <link rel="stylesheet" href="/admin.css?v=18">
<%= yield :head %> <%= yield :head %>
<% if content_for?(:replay_archive_styles) %> <% if content_for?(:replay_archive_styles) %>
<link rel="stylesheet" href="/marketing.css?v=42"> <link rel="stylesheet" href="/marketing.css?v=42">
+8 -2
View File
@@ -427,8 +427,14 @@ de:
lead: Aggregierte First-Party-Heatmaps (Bewegung/Klick) und Scrolltiefe, nur mit Statistik-Einwilligung. Keine personenbezogenen Daten. lead: Aggregierte First-Party-Heatmaps (Bewegung/Klick) und Scrolltiefe, nur mit Statistik-Einwilligung. Keine personenbezogenen Daten.
none: Keine Daten im gewählten Zeitraum. none: Keine Daten im gewählten Zeitraum.
heatmap: Heatmap heatmap: Heatmap
trend_title: Verlauf über die Zeit trend_title: Pageview-Verlauf
trend_lead: Tägliche Pageviews, Klicks und Bewegungen im gefilterten Zeitraum. trend_lead: Gesamte Pageviews aller Seiten, Tag für Tag. Dieselben Filter Von / Bis / Gerät. Nicht mit einer Tabellenzeile vergleichbar (die nach Pfad trennt).
trend_total_label: Pageviews gesamt (alle Seiten)
trend_total_hint: "%{from} → %{to}"
hide_chart: Diagramm ausblenden
show_chart: Diagramm anzeigen
pages_title: Aufschlüsselung nach Seite
pages_lead: Pageviews und Engagement im gefilterten Zeitraum, nach Pfad. Die Summe der Pageview-Zeilen entspricht dem Diagramm-Total.
table: table:
path: Seite path: Seite
pageviews: Pageviews pageviews: Pageviews
+8 -2
View File
@@ -427,8 +427,14 @@ en:
lead: Aggregated first-party move/click heatmaps and scroll depth, only with analytics consent. No personal data. lead: Aggregated first-party move/click heatmaps and scroll depth, only with analytics consent. No personal data.
none: No data in the selected period. none: No data in the selected period.
heatmap: Heatmap heatmap: Heatmap
trend_title: Trend over time trend_title: Pageview trend
trend_lead: Daily pageviews, clicks and moves for the filtered period. trend_lead: Total pageviews across all pages, day by day. Uses the same From / To / Device filters. Not comparable to a single table row (which splits by path).
trend_total_label: Total pageviews (all pages)
trend_total_hint: "%{from} → %{to}"
hide_chart: Hide chart
show_chart: Show chart
pages_title: Breakdown by page
pages_lead: Pageviews and engagement for the filtered period, split by path. The sum of pageview rows matches the chart total.
table: table:
path: Page path: Page
pageviews: Pageviews pageviews: Pageviews
+8 -2
View File
@@ -427,8 +427,14 @@ es:
lead: Heatmaps de movimientos/clics y scroll agregados (first-party), solo con consentimiento estadístico. Sin datos personales. lead: Heatmaps de movimientos/clics y scroll agregados (first-party), solo con consentimiento estadístico. Sin datos personales.
none: No hay datos en el periodo seleccionado. none: No hay datos en el periodo seleccionado.
heatmap: Heatmap heatmap: Heatmap
trend_title: Evolución en el tiempo trend_title: Evolución de pageviews
trend_lead: Pageviews, clics y movimientos día a día en el periodo filtrado. trend_lead: Total de pageviews de todas las páginas, día a día. Usa los mismos filtros Desde / Hasta / Device. No es comparable con una fila de la tabla (que parte por path).
trend_total_label: Pageviews totales (todas las páginas)
trend_total_hint: "%{from} → %{to}"
hide_chart: Ocultar gráfico
show_chart: Mostrar gráfico
pages_title: Detalle por página
pages_lead: Pageviews y engagement en el periodo filtrado, por path. La suma de las filas coincide con el total del gráfico.
table: table:
path: Página path: Página
pageviews: Pageviews pageviews: Pageviews
+8 -2
View File
@@ -427,8 +427,14 @@ fr:
lead: Heatmaps mouvements/clics et scroll agrégés (first-party), uniquement avec consentement statistiques. Aucune donnée personnelle. lead: Heatmaps mouvements/clics et scroll agrégés (first-party), uniquement avec consentement statistiques. Aucune donnée personnelle.
none: Aucune donnée sur la période sélectionnée. none: Aucune donnée sur la période sélectionnée.
heatmap: Heatmap heatmap: Heatmap
trend_title: Évolution dans le temps trend_title: Évolution des pages vues
trend_lead: Pages vues, clics et mouvements par jour sur la période filtrée. trend_lead: Total des pages vues sur toutes les pages, jour par jour. Mêmes filtres De / À / Appareil. Non comparable à une ligne du tableau (découpée par chemin).
trend_total_label: Pages vues totales (toutes les pages)
trend_total_hint: "%{from} → %{to}"
hide_chart: Masquer le graphique
show_chart: Afficher le graphique
pages_title: Détail par page
pages_lead: Pages vues et engagement sur la période filtrée, par chemin. La somme des lignes égale le total du graphique.
table: table:
path: Page path: Page
pageviews: Pages vues pageviews: Pages vues
+8 -2
View File
@@ -448,8 +448,14 @@ it:
lead: Heatmap movimenti/click e scroll aggregati (first-party), solo con consenso statistico. Nessun dato personale. lead: Heatmap movimenti/click e scroll aggregati (first-party), solo con consenso statistico. Nessun dato personale.
none: Nessun dato nel periodo selezionato. none: Nessun dato nel periodo selezionato.
heatmap: Heatmap heatmap: Heatmap
trend_title: Andamento nel tempo trend_title: Andamento pageview
trend_lead: Pageview, click e movimenti giorno per giorno nel periodo filtrato. trend_lead: Totale pageview di tutte le pagine, giorno per giorno. Usa gli stessi filtri Da / A / Device. Non è confrontabile con una singola riga della tabella (che spezza per path).
trend_total_label: Pageview totali (tutte le pagine)
trend_total_hint: "%{from} → %{to}"
hide_chart: Nascondi grafico
show_chart: Mostra grafico
pages_title: Dettaglio per pagina
pages_lead: Pageview e engagement nel periodo filtrato, spezzati per path. La somma delle pageview delle righe coincide col totale del grafico.
table: table:
path: Pagina path: Pagina
pageviews: Pageview pageviews: Pageview
+35 -27
View File
@@ -1,7 +1,40 @@
(function () { (function () {
var STORAGE_KEY = "mltv_admin_analytics_chart_hidden";
var trend = window.adminAnalyticsTrend || []; var trend = window.adminAnalyticsTrend || [];
var i18n = window.adminAnalyticsI18n || {}; var i18n = window.adminAnalyticsI18n || {};
var panel = document.getElementById("admin-analytics-trend");
var canvas = document.getElementById("chart-analytics-trend"); var canvas = document.getElementById("chart-analytics-trend");
var toggle = document.getElementById("admin-analytics-trend-toggle");
var body = document.getElementById("admin-analytics-trend-body");
function setHidden(hidden) {
if (!panel || !body || !toggle) return;
body.hidden = hidden;
panel.classList.toggle("is-collapsed", hidden);
toggle.setAttribute("aria-expanded", hidden ? "false" : "true");
toggle.textContent = hidden
? (i18n.showChart || "Mostra grafico")
: (i18n.hideChart || "Nascondi grafico");
try {
localStorage.setItem(STORAGE_KEY, hidden ? "1" : "0");
} catch (e) { /* ignore */ }
}
function initiallyHidden() {
try {
return localStorage.getItem(STORAGE_KEY) === "1";
} catch (e) {
return false;
}
}
if (toggle && body) {
setHidden(initiallyHidden());
toggle.addEventListener("click", function () {
setHidden(!body.hidden);
});
}
if (!canvas || !trend.length || typeof Chart === "undefined") return; if (!canvas || !trend.length || typeof Chart === "undefined") return;
function dayLabel(dayStr) { function dayLabel(dayStr) {
@@ -11,14 +44,10 @@
return d.toLocaleDateString([], { day: "2-digit", month: "short" }); return d.toLocaleDateString([], { day: "2-digit", month: "short" });
} }
var labels = trend.map(function (row) {
return dayLabel(row.day);
});
new Chart(canvas, { new Chart(canvas, {
type: "line", type: "line",
data: { data: {
labels: labels, labels: trend.map(function (row) { return dayLabel(row.day); }),
datasets: [ datasets: [
{ {
label: i18n.pageviews || "Pageviews", label: i18n.pageviews || "Pageviews",
@@ -29,27 +58,6 @@
tension: 0.25, tension: 0.25,
pointRadius: 3, pointRadius: 3,
pointHoverRadius: 5 pointHoverRadius: 5
},
{
label: i18n.clicks || "Clicks",
data: trend.map(function (r) { return r.clicks || 0; }),
borderColor: "rgba(66, 165, 245, 0.95)",
backgroundColor: "transparent",
fill: false,
tension: 0.25,
pointRadius: 2,
pointHoverRadius: 4
},
{
label: i18n.moves || "Moves",
data: trend.map(function (r) { return r.moves || 0; }),
borderColor: "rgba(255, 193, 7, 0.85)",
backgroundColor: "transparent",
fill: false,
tension: 0.25,
pointRadius: 2,
pointHoverRadius: 4,
borderDash: [4, 3]
} }
] ]
}, },
@@ -74,7 +82,7 @@
} }
}, },
plugins: { plugins: {
legend: { labels: { color: "#ccc", boxWidth: 12 } } legend: { display: false }
} }
} }
}); });
+21
View File
@@ -178,10 +178,31 @@ body.admin-body {
margin-top: 0.75rem; margin-top: 0.75rem;
} }
.admin-analytics-trend__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
.admin-analytics-trend__head h3 {
margin: 0 0 0.35rem;
}
.admin-analytics-trend__kpi { .admin-analytics-trend__kpi {
margin: 0.75rem 0 0; margin: 0.75rem 0 0;
} }
.admin-analytics-trend.is-collapsed .admin-analytics-trend__head {
margin-bottom: 0;
}
.admin-analytics-pages-title {
margin: 0 0 0.35rem;
font-size: 1rem;
}
.admin-panels { .admin-panels {
display: grid; display: grid;
grid-template-columns: 1.4fr 1fr; grid-template-columns: 1.4fr 1fr;
+19 -2
View File
@@ -7,6 +7,8 @@ RSpec.describe "Admin analytics", type: :request do
before do before do
post admin_login_path, params: { username: admin.username, password: "Password123" } post admin_login_path, params: { username: admin.username, password: "Password123" }
AnalyticsPageStat.delete_all
AnalyticsPageCell.delete_all
AnalyticsPageStat.create!( AnalyticsPageStat.create!(
day: Time.zone.today, day: Time.zone.today,
page_path: "/prezzi", page_path: "/prezzi",
@@ -35,9 +37,12 @@ RSpec.describe "Admin analytics", type: :request do
expect(response.body).to include("40") expect(response.body).to include("40")
expect(response.body).to include("chart-analytics-trend") expect(response.body).to include("chart-analytics-trend")
expect(response.body).to include("adminAnalyticsTrend") expect(response.body).to include("adminAnalyticsTrend")
expect(response.body).to include(I18n.t("admin.analytics.index.hide_chart"))
expect(response.body).to include("hideChart")
expect(response.body).not_to include("clicks:")
end end
it "mostra il trend anche su più giorni" do it "il grafico rispetta i filtri data e aggrega pageview di tutte le pagine" do
AnalyticsPageStat.create!( AnalyticsPageStat.create!(
day: 1.day.ago.to_date, day: 1.day.ago.to_date,
page_path: "/prezzi", page_path: "/prezzi",
@@ -47,13 +52,25 @@ RSpec.describe "Admin analytics", type: :request do
scroll_sum_pct: 0, scroll_sum_pct: 0,
max_scroll_pct: 0 max_scroll_pct: 0
) )
AnalyticsPageStat.create!(
day: Time.zone.today,
page_path: "/altro",
device: "desktop",
pageview_count: 7,
scroll_samples: 0,
scroll_sum_pct: 0,
max_scroll_pct: 0
)
get admin_analytics_path, params: { from: 2.days.ago.to_date, to: Time.zone.today } get admin_analytics_path, params: { from: 2.days.ago.to_date, to: Time.zone.today }
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(response.body).to include(I18n.t("admin.analytics.index.trend_total_label"))
expect(response.body).to include(">24<")
expect(response.body).to include("\"pageviews\":5") expect(response.body).to include("\"pageviews\":5")
expect(response.body).to include("\"pageviews\":12") expect(response.body).to include("\"pageviews\":19")
end end
it "mostra la heatmap di una pagina con tab dispositivo" do it "mostra la heatmap di una pagina con tab dispositivo" do
get admin_analytics_page_path, params: { page_path: "/prezzi" } get admin_analytics_page_path, params: { page_path: "/prezzi" }
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)