Compare commits

..
4 Commits
Author SHA1 Message Date
Emiliano FrascaroandCursor 53ad26fdbe Alza iOS a build 38 per il rilascio da produzione.
La 37 era stata caricata da collaudo: App Store Connect richiede un numero di build nuovo dal branch di produzione.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-08 21:58:09 +02:00
Emiliano FrascaroandCursor db0e842ca7 Prepara iOS 2.0.13 build 37 per lo Store con icona chiara e test del calendario.
Apple ha chiuso il treno 2.0.12: servono versione nuova, monogramma visibile sul nero e copertura dei casi dopo l'orario previsto.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-08 21:57:56 +02:00
eminuxandCursor 2e67e590d6 Aggiunge grafico andamento visite in admin Analytics.
Serie giornaliera di pageview/click/movimenti nel periodo filtrato, sopra la tabella pagine.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-08 21:52:29 +02:00
eminuxandCursor bb342ead79 Bump Android a 2.0.13-native (versionCode 34) per Play Store.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-07 08:48:08 +02:00
15 changed files with 182 additions and 12 deletions
@@ -36,6 +36,23 @@ module Admin
max_scroll: max_scroll_by_path[path].to_i
}
end.sort_by { |r| [-r[:pageviews], -r[:moves], -r[:clicks], r[:page_path]] }
pageviews_by_day = scope.group(:day).sum(:pageview_count)
clicks_by_day = cell_scope.group(:day).sum(:click_count)
moves_by_day = cell_scope.group(:day).sum(:move_count)
@trend = (@filters[:from]..@filters[:to]).map do |day|
{
day: day.iso8601,
pageviews: pageviews_by_day[day].to_i,
clicks: clicks_by_day[day].to_i,
moves: moves_by_day[day].to_i
}
end
@trend_totals = {
pageviews: @trend.sum { |r| r[:pageviews] },
clicks: @trend.sum { |r| r[:clicks] },
moves: @trend.sum { |r| r[:moves] }
}
end
def show
@@ -58,6 +58,40 @@
<% end %>
</div>
<% if @trend.any? && (@trend_totals[:pageviews].positive? || @trend_totals[:clicks].positive? || @trend_totals[:moves].positive?) %>
<section class="panel admin-analytics-trend">
<h3><%= t("admin.analytics.index.trend_title") %></h3>
<p class="muted admin-table-sub"><%= t("admin.analytics.index.trend_lead") %></p>
<div class="kpi-grid admin-analytics-trend__kpi">
<div class="kpi">
<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>
<div class="chart-wrap chart-wrap--analytics">
<canvas id="chart-analytics-trend" aria-label="<%= t("admin.analytics.index.trend_title") %>"></canvas>
</div>
</section>
<script>
window.adminAnalyticsTrend = <%= raw @trend.to_json %>;
window.adminAnalyticsI18n = {
pageviews: <%= raw t("admin.analytics.index.table.pageviews").to_json %>,
clicks: <%= raw t("admin.analytics.index.table.clicks").to_json %>,
moves: <%= raw t("admin.analytics.index.table.moves").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=1" defer></script>
<% end %>
<div class="panel">
<% if @pages.any? %>
<div class="admin-table-wrap">
+1 -1
View File
@@ -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=17">
<%= yield :head %>
<% if content_for?(:replay_archive_styles) %>
<link rel="stylesheet" href="/marketing.css?v=42">
+2
View File
@@ -427,6 +427,8 @@ 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
trend_title: Verlauf über die Zeit
trend_lead: Tägliche Pageviews, Klicks und Bewegungen im gefilterten Zeitraum.
table:
path: Seite
pageviews: Pageviews
+2
View File
@@ -427,6 +427,8 @@ 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
trend_title: Trend over time
trend_lead: Daily pageviews, clicks and moves for the filtered period.
table:
path: Page
pageviews: Pageviews
+2
View File
@@ -427,6 +427,8 @@ 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
trend_title: Evolución en el tiempo
trend_lead: Pageviews, clics y movimientos día a día en el periodo filtrado.
table:
path: Página
pageviews: Pageviews
+2
View File
@@ -427,6 +427,8 @@ 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
trend_title: Évolution dans le temps
trend_lead: Pages vues, clics et mouvements par jour sur la période filtrée.
table:
path: Page
pageviews: Pages vues
+2
View File
@@ -448,6 +448,8 @@ 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
trend_title: Andamento nel tempo
trend_lead: Pageview, click e movimenti giorno per giorno nel periodo filtrato.
table:
path: Pagina
pageviews: Pageview
+81
View File
@@ -0,0 +1,81 @@
(function () {
var trend = window.adminAnalyticsTrend || [];
var i18n = window.adminAnalyticsI18n || {};
var canvas = document.getElementById("chart-analytics-trend");
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" });
}
var labels = trend.map(function (row) {
return dayLabel(row.day);
});
new Chart(canvas, {
type: "line",
data: {
labels: labels,
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
},
{
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]
}
]
},
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: { labels: { color: "#ccc", boxWidth: 12 } }
}
}
});
})();
+9
View File
@@ -173,6 +173,15 @@ body.admin-body {
height: 200px;
}
.chart-wrap--analytics {
height: 260px;
margin-top: 0.75rem;
}
.admin-analytics-trend__kpi {
margin: 0.75rem 0 0;
}
.admin-panels {
display: grid;
grid-template-columns: 1.4fr 1fr;
@@ -33,6 +33,25 @@ 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")
end
it "mostra il trend anche su più giorni" 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
)
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("\"pageviews\":5")
expect(response.body).to include("\"pageviews\":12")
end
it "mostra la heatmap di una pagina con tab dispositivo" do
+2 -2
View File
@@ -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,7 +677,7 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CLANG_ENABLE_OBJC_WEAK = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 37;
CURRENT_PROJECT_VERSION = 38;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_API_BASE_URL = "$(API_BASE_URL)";
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
@@ -698,7 +698,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_OBJC_WEAK = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 37;
CURRENT_PROJECT_VERSION = 38;
DEVELOPMENT_TEAM = S8Q9TWBRG5;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = MatchLiveTv/Resources/Info.plist;
@@ -727,7 +727,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_OBJC_WEAK = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 37;
CURRENT_PROJECT_VERSION = 38;
DEVELOPMENT_TEAM = S8Q9TWBRG5;
ENABLE_TESTABILITY = YES;
GENERATE_INFOPLIST_FILE = NO;
@@ -756,7 +756,7 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CLANG_ENABLE_OBJC_WEAK = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 37;
CURRENT_PROJECT_VERSION = 38;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_API_BASE_URL = "$(API_BASE_URL)";
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
@@ -775,7 +775,7 @@
buildSettings = {
CLANG_ENABLE_OBJC_WEAK = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 37;
CURRENT_PROJECT_VERSION = 38;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_API_BASE_URL = "$(API_BASE_URL)";
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
@@ -794,7 +794,7 @@
buildSettings = {
CLANG_ENABLE_OBJC_WEAK = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 37;
CURRENT_PROJECT_VERSION = 38;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_API_BASE_URL = "$(API_BASE_URL)";
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
+1 -1
View File
@@ -32,7 +32,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>37</string>
<string>38</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
+2 -2
View File
@@ -96,7 +96,7 @@ lines += [
app_settings = """
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 37;
CURRENT_PROJECT_VERSION = 38;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = MatchLiveTv/Resources/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
@@ -122,7 +122,7 @@ app_debug_settings = app_settings + """
test_settings = """
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 37;
CURRENT_PROJECT_VERSION = 38;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MARKETING_VERSION = 2.0.13;