Compare commits
6
Commits
collaudo
...
adfea01cdb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
adfea01cdb | ||
|
|
81e7e3c9af | ||
|
|
53ad26fdbe | ||
|
|
db0e842ca7 | ||
|
|
2e67e590d6 | ||
|
|
bb342ead79 |
@@ -6,7 +6,8 @@ module Admin
|
||||
@filters = {
|
||||
from: parse_date(params[:from]) || 7.days.ago.to_date,
|
||||
to: parse_date(params[:to]) || Time.zone.today,
|
||||
device: params[:device].presence
|
||||
device: params[:device].presence,
|
||||
chart_path: params[:chart_path].presence
|
||||
}
|
||||
@analytics_preview_active = analytics_preview_active?
|
||||
|
||||
@@ -36,6 +37,24 @@ module Admin
|
||||
max_scroll: max_scroll_by_path[path].to_i
|
||||
}
|
||||
end.sort_by { |r| [-r[:pageviews], -r[:moves], -r[:clicks], r[:page_path]] }
|
||||
|
||||
@chart_path_options = @pages.map { |row| row[:page_path] }
|
||||
if @filters[:chart_path].present? && @chart_path_options.exclude?(@filters[:chart_path])
|
||||
@filters[:chart_path] = nil
|
||||
end
|
||||
|
||||
trend_scope = scope
|
||||
trend_scope = trend_scope.where(page_path: @filters[:chart_path]) if @filters[:chart_path].present?
|
||||
pageviews_by_day = trend_scope.group(:day).sum(:pageview_count)
|
||||
@trend = (@filters[:from]..@filters[:to]).map do |day|
|
||||
{
|
||||
day: day.iso8601,
|
||||
pageviews: pageviews_by_day[day].to_i
|
||||
}
|
||||
end
|
||||
@trend_totals = {
|
||||
pageviews: @trend.sum { |r| r[:pageviews] }
|
||||
}
|
||||
end
|
||||
|
||||
def show
|
||||
|
||||
@@ -50,6 +50,14 @@
|
||||
@filters[:device]
|
||||
) %>
|
||||
</label>
|
||||
<label class="admin-filter-field">
|
||||
<span><%= t("admin.analytics.filters.chart_path") %></span>
|
||||
<%= select_tag :chart_path,
|
||||
options_for_select(
|
||||
[[t("admin.analytics.filters.chart_path_all"), ""]] + @chart_path_options.map { |p| [p, p] },
|
||||
@filters[:chart_path]
|
||||
) %>
|
||||
</label>
|
||||
</div>
|
||||
<div class="admin-filter-actions">
|
||||
<%= submit_tag t("admin.analytics.filters.apply"), class: "admin-btn admin-btn--primary admin-btn--sm" %>
|
||||
@@ -58,8 +66,66 @@
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if @trend.any? && @trend_totals[:pageviews].positive? %>
|
||||
<section class="panel admin-analytics-trend" id="admin-analytics-trend">
|
||||
<div class="admin-analytics-trend__head">
|
||||
<div>
|
||||
<h3><%= t("admin.analytics.index.trend_title") %></h3>
|
||||
<p class="muted admin-table-sub">
|
||||
<% if @filters[:chart_path].present? %>
|
||||
<%= t("admin.analytics.index.trend_lead_path", path: @filters[:chart_path]) %>
|
||||
<% else %>
|
||||
<%= t("admin.analytics.index.trend_lead") %>
|
||||
<% end %>
|
||||
</p>
|
||||
</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 id="admin-analytics-trend-body">
|
||||
<div class="kpi-grid admin-analytics-trend__kpi">
|
||||
<div class="kpi">
|
||||
<div class="kpi-label">
|
||||
<% if @filters[:chart_path].present? %>
|
||||
<%= t("admin.analytics.index.trend_total_label_path") %>
|
||||
<% else %>
|
||||
<%= t("admin.analytics.index.trend_total_label") %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="kpi-value"><%= @trend_totals[:pageviews] %></div>
|
||||
<div class="kpi-sub muted">
|
||||
<% if @filters[:chart_path].present? %>
|
||||
<code class="admin-mono"><%= @filters[:chart_path] %></code>
|
||||
·
|
||||
<% end %>
|
||||
<%= 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>
|
||||
</section>
|
||||
<script>
|
||||
window.adminAnalyticsTrend = <%= raw @trend.to_json %>;
|
||||
window.adminAnalyticsI18n = {
|
||||
pageviews: <%= raw t("admin.analytics.index.table.pageviews").to_json %>,
|
||||
hideChart: <%= raw t("admin.analytics.index.hide_chart").to_json %>,
|
||||
showChart: <%= raw t("admin.analytics.index.show_chart").to_json %>
|
||||
};
|
||||
</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=3" defer></script>
|
||||
<% end %>
|
||||
|
||||
<div class="panel">
|
||||
<% 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">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
@@ -76,14 +142,18 @@
|
||||
<tbody>
|
||||
<% @pages.each do |row| %>
|
||||
<% avg = row[:scroll_samples].positive? ? (row[:scroll_sum].to_f / row[:scroll_samples]).round : 0 %>
|
||||
<tr>
|
||||
<% chart_params = { from: @filters[:from], to: @filters[:to], chart_path: row[:page_path] } %>
|
||||
<% chart_params[:device] = @filters[:device] if @filters[:device].present? %>
|
||||
<tr class="<%= "is-chart-focus" if @filters[:chart_path] == row[:page_path] %>">
|
||||
<td><code class="admin-mono"><%= row[:page_path] %></code></td>
|
||||
<td><%= row[:pageviews] %></td>
|
||||
<td><%= row[:moves] %></td>
|
||||
<td><%= row[:clicks] %></td>
|
||||
<td class="muted"><%= avg %>%</td>
|
||||
<td class="muted"><%= row[:max_scroll] %>%</td>
|
||||
<td>
|
||||
<td class="admin-analytics-row-actions">
|
||||
<%= link_to t("admin.analytics.index.chart_for_path"), admin_analytics_path(chart_params) %>
|
||||
·
|
||||
<% heatmap_params = { page_path: row[:page_path], from: @filters[:from], to: @filters[:to] } %>
|
||||
<% heatmap_params[:device] = @filters[:device] if @filters[:device].present? %>
|
||||
<%= link_to t("admin.analytics.index.heatmap"), admin_analytics_page_path(heatmap_params) %>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<%= csrf_meta_tags %>
|
||||
<link rel="stylesheet" href="/admin.css?v=16">
|
||||
<link rel="stylesheet" href="/admin.css?v=19">
|
||||
<%= yield :head %>
|
||||
<% if content_for?(:replay_archive_styles) %>
|
||||
<link rel="stylesheet" href="/marketing.css?v=42">
|
||||
|
||||
@@ -415,6 +415,8 @@ de:
|
||||
any: Alle
|
||||
apply: Filtern
|
||||
reset: Zurücksetzen
|
||||
chart_path: Seite (Diagramm)
|
||||
chart_path_all: Alle Seiten
|
||||
layer: Ebene
|
||||
layer_move: Mausbewegungen
|
||||
layer_click: Klicks
|
||||
@@ -427,6 +429,17 @@ de:
|
||||
lead: Aggregierte First-Party-Heatmaps (Bewegung/Klick) und Scrolltiefe, nur mit Statistik-Einwilligung. Keine personenbezogenen Daten.
|
||||
none: Keine Daten im gewählten Zeitraum.
|
||||
heatmap: Heatmap
|
||||
chart_for_path: Diagramm
|
||||
trend_title: Pageview-Verlauf
|
||||
trend_lead: Gesamte Pageviews aller Seiten, Tag für Tag. Dieselben Filter Von / Bis / Gerät. Seite im Filter wählen oder «Diagramm» in der Tabelle.
|
||||
trend_lead_path: "Tägliche Pageviews nur für %{path} (gleiche Filter Von / Bis / Gerät)."
|
||||
trend_total_label: Pageviews gesamt (alle Seiten)
|
||||
trend_total_label_path: Pageviews der gewählten Seite
|
||||
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. «Diagramm» zeigt den Verlauf einer Seite.
|
||||
table:
|
||||
path: Seite
|
||||
pageviews: Pageviews
|
||||
|
||||
@@ -415,6 +415,8 @@ en:
|
||||
any: All
|
||||
apply: Filter
|
||||
reset: Reset
|
||||
chart_path: Page (chart)
|
||||
chart_path_all: All pages
|
||||
layer: Layer
|
||||
layer_move: Mouse moves
|
||||
layer_click: Clicks
|
||||
@@ -427,6 +429,17 @@ en:
|
||||
lead: Aggregated first-party move/click heatmaps and scroll depth, only with analytics consent. No personal data.
|
||||
none: No data in the selected period.
|
||||
heatmap: Heatmap
|
||||
chart_for_path: Chart
|
||||
trend_title: Pageview trend
|
||||
trend_lead: Total pageviews across all pages, day by day. Uses the same From / To / Device filters. Pick a page in the filter or click «Chart» in the table.
|
||||
trend_lead_path: "Day-by-day pageviews for %{path} only (same From / To / Device filters)."
|
||||
trend_total_label: Total pageviews (all pages)
|
||||
trend_total_label_path: Pageviews for selected page
|
||||
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. Use «Chart» to see one page over time.
|
||||
table:
|
||||
path: Page
|
||||
pageviews: Pageviews
|
||||
|
||||
@@ -415,6 +415,8 @@ es:
|
||||
any: Todos
|
||||
apply: Filtrar
|
||||
reset: Restablecer
|
||||
chart_path: Página (gráfico)
|
||||
chart_path_all: Todas las páginas
|
||||
layer: Capa
|
||||
layer_move: Movimientos del ratón
|
||||
layer_click: Clics
|
||||
@@ -427,6 +429,17 @@ es:
|
||||
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.
|
||||
heatmap: Heatmap
|
||||
chart_for_path: Gráfico
|
||||
trend_title: Evolución de pageviews
|
||||
trend_lead: Total de pageviews de todas las páginas, día a día. Usa los mismos filtros Desde / Hasta / Device. Elige una página en el filtro o pulsa «Gráfico» en la tabla.
|
||||
trend_lead_path: "Pageviews día a día solo para %{path} (mismos filtros Desde / Hasta / Device)."
|
||||
trend_total_label: Pageviews totales (todas las páginas)
|
||||
trend_total_label_path: Pageviews de la página seleccionada
|
||||
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. Usa «Gráfico» para ver una sola página en el tiempo.
|
||||
table:
|
||||
path: Página
|
||||
pageviews: Pageviews
|
||||
|
||||
@@ -415,6 +415,8 @@ fr:
|
||||
any: Tous
|
||||
apply: Filtrer
|
||||
reset: Réinitialiser
|
||||
chart_path: Page (graphique)
|
||||
chart_path_all: Toutes les pages
|
||||
layer: Couche
|
||||
layer_move: Mouvements souris
|
||||
layer_click: Clics
|
||||
@@ -427,6 +429,17 @@ fr:
|
||||
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.
|
||||
heatmap: Heatmap
|
||||
chart_for_path: Graphique
|
||||
trend_title: Évolution des pages vues
|
||||
trend_lead: Total des pages vues sur toutes les pages, jour par jour. Mêmes filtres De / À / Appareil. Choisissez une page dans le filtre ou cliquez « Graphique » dans le tableau.
|
||||
trend_lead_path: "Pages vues jour par jour pour %{path} uniquement (mêmes filtres De / À / Appareil)."
|
||||
trend_total_label: Pages vues totales (toutes les pages)
|
||||
trend_total_label_path: Pages vues de la page sélectionnée
|
||||
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. « Graphique » montre l’évolution d’une page.
|
||||
table:
|
||||
path: Page
|
||||
pageviews: Pages vues
|
||||
|
||||
@@ -436,6 +436,8 @@ it:
|
||||
any: Tutti
|
||||
apply: Filtra
|
||||
reset: Azzera
|
||||
chart_path: Pagina (grafico)
|
||||
chart_path_all: Tutte le pagine
|
||||
layer: Livello
|
||||
layer_move: Movimenti mouse
|
||||
layer_click: Click
|
||||
@@ -448,6 +450,17 @@ it:
|
||||
lead: Heatmap movimenti/click e scroll aggregati (first-party), solo con consenso statistico. Nessun dato personale.
|
||||
none: Nessun dato nel periodo selezionato.
|
||||
heatmap: Heatmap
|
||||
chart_for_path: Grafico
|
||||
trend_title: Andamento pageview
|
||||
trend_lead: Totale pageview di tutte le pagine, giorno per giorno. Usa gli stessi filtri Da / A / Device. Scegli una pagina nel filtro oppure clicca «Grafico» in tabella.
|
||||
trend_lead_path: "Pageview giorno per giorno solo per %{path} (stessi filtri Da / A / Device)."
|
||||
trend_total_label: Pageview totali (tutte le pagine)
|
||||
trend_total_label_path: Pageview della pagina selezionata
|
||||
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. Usa «Grafico» per vedere l’andamento di una sola pagina.
|
||||
table:
|
||||
path: Pagina
|
||||
pageviews: Pageview
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
(function () {
|
||||
var STORAGE_KEY = "mltv_admin_analytics_chart_hidden";
|
||||
var trend = window.adminAnalyticsTrend || [];
|
||||
var i18n = window.adminAnalyticsI18n || {};
|
||||
var panel = document.getElementById("admin-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;
|
||||
|
||||
function dayLabel(dayStr) {
|
||||
var parts = dayStr.split("-");
|
||||
if (parts.length < 3) return dayStr;
|
||||
var d = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10));
|
||||
return d.toLocaleDateString([], { day: "2-digit", month: "short" });
|
||||
}
|
||||
|
||||
new Chart(canvas, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: trend.map(function (row) { return dayLabel(row.day); }),
|
||||
datasets: [
|
||||
{
|
||||
label: i18n.pageviews || "Pageviews",
|
||||
data: trend.map(function (r) { return r.pageviews || 0; }),
|
||||
borderColor: "rgba(229, 57, 53, 0.95)",
|
||||
backgroundColor: "rgba(229, 57, 53, 0.18)",
|
||||
fill: true,
|
||||
tension: 0.25,
|
||||
pointRadius: 3,
|
||||
pointHoverRadius: 5
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: { mode: "index", intersect: false },
|
||||
animation: { duration: 280 },
|
||||
scales: {
|
||||
x: {
|
||||
ticks: { maxTicksLimit: 14, color: "#9a9aad", font: { size: 10 } },
|
||||
grid: { color: "rgba(255,255,255,0.06)" }
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
color: "#9a9aad",
|
||||
font: { size: 10 },
|
||||
precision: 0
|
||||
},
|
||||
grid: { color: "rgba(255,255,255,0.06)" }
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false }
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -173,6 +173,44 @@ body.admin-body {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.chart-wrap--analytics {
|
||||
height: 260px;
|
||||
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 {
|
||||
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-table tr.is-chart-focus td {
|
||||
background: rgba(229, 57, 53, 0.12);
|
||||
}
|
||||
|
||||
.admin-analytics-row-actions {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.admin-panels {
|
||||
display: grid;
|
||||
grid-template-columns: 1.4fr 1fr;
|
||||
|
||||
@@ -7,6 +7,8 @@ RSpec.describe "Admin analytics", type: :request do
|
||||
|
||||
before do
|
||||
post admin_login_path, params: { username: admin.username, password: "Password123" }
|
||||
AnalyticsPageStat.delete_all
|
||||
AnalyticsPageCell.delete_all
|
||||
AnalyticsPageStat.create!(
|
||||
day: Time.zone.today,
|
||||
page_path: "/prezzi",
|
||||
@@ -33,8 +35,62 @@ RSpec.describe "Admin analytics", type: :request do
|
||||
expect(response.body).to include("/prezzi")
|
||||
expect(response.body).to include("12")
|
||||
expect(response.body).to include("40")
|
||||
expect(response.body).to include("chart-analytics-trend")
|
||||
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
|
||||
|
||||
it "il grafico rispetta i filtri data e aggrega pageview di tutte le pagine" do
|
||||
AnalyticsPageStat.create!(
|
||||
day: 1.day.ago.to_date,
|
||||
page_path: "/prezzi",
|
||||
device: "desktop",
|
||||
pageview_count: 5,
|
||||
scroll_samples: 0,
|
||||
scroll_sum_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 }
|
||||
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\":19")
|
||||
end
|
||||
|
||||
it "filtra il grafico su una sola pagina" do
|
||||
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: { chart_path: "/prezzi" }
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to include(I18n.t("admin.analytics.index.trend_total_label_path"))
|
||||
expect(response.body).to include(">12<")
|
||||
expect(response.body).to include(I18n.t("admin.analytics.index.chart_for_path"))
|
||||
expect(response.body).to include("is-chart-focus")
|
||||
expect(response.body).not_to include(">19<")
|
||||
end
|
||||
|
||||
|
||||
it "mostra la heatmap di una pagina con tab dispositivo" do
|
||||
get admin_analytics_page_path, params: { page_path: "/prezzi" }
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
@@ -24,8 +24,8 @@ android {
|
||||
applicationId = "com.matchlivetv.match_live_tv"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = 33
|
||||
versionName = "2.0.12-native"
|
||||
versionCode = 34
|
||||
versionName = "2.0.13-native"
|
||||
|
||||
val apiBaseUrl = project.findProperty("API_BASE_URL") as String?
|
||||
?: "https://www.matchlivetv.it"
|
||||
|
||||
@@ -677,11 +677,11 @@
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 35;
|
||||
CURRENT_PROJECT_VERSION = 38;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_API_BASE_URL = "$(API_BASE_URL)";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
MARKETING_VERSION = 2.0.12;
|
||||
MARKETING_VERSION = 2.0.13;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.matchlivetv.match-live-tv.tests";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -698,7 +698,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 35;
|
||||
CURRENT_PROJECT_VERSION = 38;
|
||||
DEVELOPMENT_TEAM = S8Q9TWBRG5;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = MatchLiveTv/Resources/Info.plist;
|
||||
@@ -709,7 +709,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.0.12;
|
||||
MARKETING_VERSION = 2.0.13;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.matchlivetv.match-live-tv";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -727,7 +727,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 35;
|
||||
CURRENT_PROJECT_VERSION = 38;
|
||||
DEVELOPMENT_TEAM = S8Q9TWBRG5;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
@@ -739,7 +739,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.0.12;
|
||||
MARKETING_VERSION = 2.0.13;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.matchlivetv.match-live-tv";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -756,11 +756,11 @@
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 35;
|
||||
CURRENT_PROJECT_VERSION = 38;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_API_BASE_URL = "$(API_BASE_URL)";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
MARKETING_VERSION = 2.0.12;
|
||||
MARKETING_VERSION = 2.0.13;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.matchlivetv.match-live-tv.tests";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -775,11 +775,11 @@
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 35;
|
||||
CURRENT_PROJECT_VERSION = 38;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_API_BASE_URL = "$(API_BASE_URL)";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
MARKETING_VERSION = 2.0.12;
|
||||
MARKETING_VERSION = 2.0.13;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.matchlivetv.match-live-tv.uitests";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -794,11 +794,11 @@
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 35;
|
||||
CURRENT_PROJECT_VERSION = 38;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_API_BASE_URL = "$(API_BASE_URL)";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
MARKETING_VERSION = 2.0.12;
|
||||
MARKETING_VERSION = 2.0.13;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.matchlivetv.match-live-tv.uitests";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 215 KiB |
@@ -19,7 +19,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.0.12</string>
|
||||
<string>2.0.13</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
@@ -32,7 +32,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>35</string>
|
||||
<string>38</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
|
||||
@@ -1,7 +1,45 @@
|
||||
import XCTest
|
||||
@testable import MatchLiveTv
|
||||
|
||||
/// Smoke test eseguibile senza @testable import (validazione logica pura).
|
||||
/// Logica calendario/API reale + smoke parser (RTMP/overlay) senza dipendenze di rete.
|
||||
final class ApiInstantTests: XCTestCase {
|
||||
func testParseApiInstantRailsOffsetWithMillis() {
|
||||
let now = iso("2026-06-06T16:00:00Z")
|
||||
XCTAssertTrue(ApiInstant.isScheduledOnCalendar("2026-06-06T20:00:00.000+02:00", now: now))
|
||||
}
|
||||
|
||||
func testIsScheduledOnCalendarKeepsThisMorningAfterKickoff() {
|
||||
let kickoff = "2026-09-06T07:00:00Z"
|
||||
let now = iso("2026-09-06T08:30:00Z")
|
||||
XCTAssertTrue(ApiInstant.isScheduledOnCalendar(kickoff, now: now))
|
||||
XCTAssertFalse(ApiInstant.isScheduledFuture(kickoff, now: now))
|
||||
}
|
||||
|
||||
func testIsScheduledOnCalendarHidesYesterday() {
|
||||
let yesterday = "2026-09-05T10:00:00Z"
|
||||
let now = iso("2026-09-06T08:30:00Z")
|
||||
XCTAssertFalse(ApiInstant.isScheduledOnCalendar(yesterday, now: now))
|
||||
}
|
||||
|
||||
func testConcurrentStreamForbiddenShowsApiMessage() {
|
||||
let body = """
|
||||
{"error":"Hai già una diretta in corso con questo account. Chiudila prima di avviarne un’altra.","error_code":"user_concurrent_stream"}
|
||||
"""
|
||||
XCTAssertEqual(
|
||||
APIError.http(403, body).errorDescription,
|
||||
"Hai già una diretta in corso con questo account. Chiudila prima di avviarne un’altra."
|
||||
)
|
||||
}
|
||||
|
||||
private func iso(_ value: String) -> Date {
|
||||
let f = ISO8601DateFormatter()
|
||||
f.formatOptions = [.withInternetDateTime]
|
||||
guard let date = f.date(from: value) else {
|
||||
XCTFail("Data ISO non valida: \(value)")
|
||||
return Date()
|
||||
}
|
||||
return date
|
||||
}
|
||||
func testOverlayKindMapping() {
|
||||
XCTAssertEqual(OverlayKindMapping.fromApi("basket"), "basket")
|
||||
XCTAssertEqual(OverlayKindMapping.fromApi("none"), "none")
|
||||
|
||||
@@ -121,6 +121,65 @@ final class MatchScoringRulesTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
final class MatchHubCalendarTests: XCTestCase {
|
||||
func testHubKeepsMatchAfterKickoffSameDay() {
|
||||
let now = iso("2026-09-06T08:30:00Z")
|
||||
let match = hubMatch(scheduledAt: "2026-09-06T07:00:00Z")
|
||||
XCTAssertTrue(MatchHubFilter.coachHubVisible(match, now: now))
|
||||
XCTAssertTrue(MatchHubFilter.isOnCalendar(match, now: now))
|
||||
XCTAssertFalse(MatchHubFilter.isScheduledFuture(match, now: now))
|
||||
}
|
||||
|
||||
func testHubHidesYesterdayMatch() {
|
||||
let now = iso("2026-09-06T08:30:00Z")
|
||||
let match = hubMatch(scheduledAt: "2026-09-05T10:00:00Z")
|
||||
XCTAssertFalse(MatchHubFilter.coachHubVisible(match, now: now))
|
||||
}
|
||||
|
||||
func testHubRespectsApiCoachHubVisibleFalse() {
|
||||
let now = iso("2026-09-06T08:30:00Z")
|
||||
let match = hubMatch(scheduledAt: "2026-09-06T07:00:00Z", coachHubVisible: false)
|
||||
XCTAssertFalse(MatchHubFilter.coachHubVisible(match, now: now))
|
||||
}
|
||||
|
||||
private func iso(_ value: String) -> Date {
|
||||
let f = ISO8601DateFormatter()
|
||||
f.formatOptions = [.withInternetDateTime]
|
||||
return f.date(from: value)!
|
||||
}
|
||||
|
||||
private func hubMatch(scheduledAt: String?, coachHubVisible: Bool? = nil) -> Match {
|
||||
Match(
|
||||
id: "match-hub",
|
||||
teamId: "team-1",
|
||||
teamName: "Team MLTV",
|
||||
opponentName: "Team Guest",
|
||||
location: nil,
|
||||
scheduledAt: scheduledAt,
|
||||
sportKey: "pallavolo",
|
||||
sportLabel: "Pallavolo",
|
||||
boardType: "volley",
|
||||
overlayKind: "volley",
|
||||
effectiveOverlayKind: "volley",
|
||||
setsToWin: 3,
|
||||
category: nil,
|
||||
scoringRules: nil,
|
||||
activeSessionId: nil,
|
||||
activeSessionStatus: nil,
|
||||
streamCompleted: false,
|
||||
coachHubVisible: coachHubVisible,
|
||||
homePrimaryColor: "#FF2D2D",
|
||||
homeSecondaryColor: nil,
|
||||
homeLogoUrl: nil,
|
||||
opponentPrimaryColor: "#1E3A8A",
|
||||
opponentLogoUrl: nil,
|
||||
effectiveCoverUrl: nil,
|
||||
coverSource: "default",
|
||||
customCoverEnabled: false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
final class ScoreboardOverlayTests: XCTestCase {
|
||||
func testScoreboardColumnsAfterSetClosed() {
|
||||
let score = ScoreState(
|
||||
|
||||
@@ -96,7 +96,7 @@ lines += [
|
||||
app_settings = """
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 35;
|
||||
CURRENT_PROJECT_VERSION = 38;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = MatchLiveTv/Resources/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
@@ -107,7 +107,7 @@ app_settings = """
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.0.12;
|
||||
MARKETING_VERSION = 2.0.13;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.matchlivetv.match-live-tv;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
API_BASE_URL = "https://www.matchlivetv.it";
|
||||
@@ -122,10 +122,10 @@ app_debug_settings = app_settings + """
|
||||
test_settings = """
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 35;
|
||||
CURRENT_PROJECT_VERSION = 38;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MARKETING_VERSION = 2.0.12;
|
||||
MARKETING_VERSION = 2.0.13;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.matchlivetv.match-live-tv.tests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
||||
Reference in New Issue
Block a user