Aggiunge cookie policy, banner consenso e Google Analytics opzionale.

Il banner consente solo necessari o tutti i cookie; GA4 si carica solo dopo
consenso. Aggiornati privacy, footer, sitemap e variabile GOOGLE_ANALYTICS_MEASUREMENT_ID.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-03 08:00:30 +02:00
parent 1f273f849d
commit 565bc2c548
21 changed files with 426 additions and 7 deletions

View File

@@ -30,6 +30,9 @@ module Public
def terms
end
def cookies
end
def faq
end

View File

@@ -10,6 +10,7 @@ module Public
{ loc: "#{base}/faq", changefreq: "monthly", priority: "0.8" },
{ loc: "#{base}/live", changefreq: "hourly", priority: "0.85" },
{ loc: "#{base}/privacy", changefreq: "yearly", priority: "0.3" },
{ loc: "#{base}/cookie", changefreq: "yearly", priority: "0.3" },
{ loc: "#{base}/termini", changefreq: "yearly", priority: "0.3" }
]

View File

@@ -1,5 +1,9 @@
module LegalHelper
def legal_last_updated
"26 maggio 2026"
"3 giugno 2026"
end
def cookie_policy_last_updated
"3 giugno 2026"
end
end

View File

@@ -8,9 +8,10 @@
<%= render "shared/meta_tags" %>
<%= yield :head %>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer">
<link rel="stylesheet" href="/marketing.css?v=35">
<link rel="stylesheet" href="/marketing.css?v=36">
</head>
<body>
<body<% if MatchLiveTv.google_analytics_configured? %> data-ga-id="<%= MatchLiveTv.google_analytics_measurement_id %>"<% end %>>
<%= render "shared/cookie_banner" %>
<%= render "shared/marketing_nav" %>
<main>
<% if flash[:notice] %><div class="wrap"><div class="flash notice"><%= flash[:notice] %></div></div><% end %>
@@ -21,5 +22,6 @@
<script src="/branding-form.js?v=1" defer></script>
<script src="/roster-form.js?v=1" defer></script>
<script src="/password-toggle.js?v=2" defer></script>
<script src="/cookie-consent.js?v=1" defer></script>
</body>
</html>

View File

@@ -6,15 +6,17 @@
<title><%= content_for?(:title) ? yield(:title) : "Match Live TV" %></title>
<%= render "shared/meta_tags" %>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer">
<link rel="stylesheet" href="/marketing.css?v=34">
<link rel="stylesheet" href="/marketing.css?v=36">
<link rel="stylesheet" href="/live.css?v=4">
<%= yield :head %>
</head>
<body>
<body<% if MatchLiveTv.google_analytics_configured? %> data-ga-id="<%= MatchLiveTv.google_analytics_measurement_id %>"<% end %>>
<%= render "shared/cookie_banner" %>
<%= render "shared/marketing_nav" %>
<main class="live-main">
<%= yield %>
</main>
<%= render "shared/marketing_footer" %>
<script src="/cookie-consent.js?v=1" defer></script>
</body>
</html>

View File

@@ -0,0 +1,150 @@
<% content_for :title, "Cookie policy — Match Live TV" %>
<% content_for :meta_description, "Cookie policy di Match Live TV: cookie tecnici, sessione, Google Analytics e gestione del consenso." %>
<% content_for :canonical_url, seo_absolute_url(public_cookies_path) %>
<div class="wrap legal-doc">
<h1>Cookie policy</h1>
<p class="legal-meta">
Ultimo aggiornamento: <%= cookie_policy_last_updated %>
· Vedi anche <%= link_to "informativa privacy", public_privacy_path %>
</p>
<section>
<h2>1. Cosa sono i cookie</h2>
<p>
I cookie sono piccoli file di testo che i siti salvano sul dispositivo del visitatore.
Servono a far funzionare il sito, ricordare preferenze o, previo consenso, analizzare luso del servizio.
</p>
</section>
<section>
<h2>2. Titolare</h2>
<p>
<strong><%= MatchLiveTv.privacy_controller_name %></strong> —
<%= MatchLiveTv.privacy_controller_address %> —
<a href="mailto:<%= MatchLiveTv.privacy_controller_email %>"><%= MatchLiveTv.privacy_controller_email %></a>
<% if MatchLiveTv.privacy_controller_vat.present? %>
· P. IVA <%= MatchLiveTv.privacy_controller_vat %>
<% end %>
</p>
</section>
<section>
<h2>3. Come gestiamo il consenso</h2>
<p>
Al primo accesso mostriamo un banner che ti permette di accettare tutti i cookie,
usare <strong>solo quelli necessari</strong> o aprire questa pagina per maggiori dettagli.
Puoi modificare la scelta in qualsiasi momento con il link
<strong>«Gestisci cookie»</strong> nel footer.
</p>
<p>
La preferenza viene memorizzata in un cookie tecnico di prima parte
(<code>mltv_cookie_consent</code>) e in <code>localStorage</code> del browser.
</p>
</section>
<section>
<h2>4. Cookie e tecnologie che utilizziamo</h2>
<h3>4.1 Strettamente necessari (sempre attivi)</h3>
<p>Non richiedono consenso perché indispensabili al funzionamento del sito e dellarea riservata.</p>
<table class="legal-table">
<thead>
<tr><th>Nome / tipo</th><th>Finalità</th><th>Durata</th><th>Fornitore</th></tr>
</thead>
<tbody>
<tr>
<td><code>_app_session</code> (cookie di sessione)</td>
<td>Mantiene laccesso dellutente registrato, protezione CSRF, preferenze di navigazione sicura</td>
<td>Sessione / fino a chiusura browser</td>
<td>Match Live TV (prima parte)</td>
</tr>
<tr>
<td><code>mltv_cookie_consent</code></td>
<td>Memorizza le scelte sul banner cookie (necessari / analytics)</td>
<td>12 mesi</td>
<td>Match Live TV (prima parte)</td>
</tr>
</tbody>
</table>
<h3>4.2 Analytics (solo con il tuo consenso)</h3>
<p>
Se accetti «Tutti i cookie» o abiliti le statistiche nel banner, carichiamo
<strong>Google Analytics 4</strong> per capire come viene usato il sito (pagine visitate, provenienza aggregata, dispositivo).
I dati sono trattati da Google Ireland Limited / Google LLC secondo le loro policy.
</p>
<% if MatchLiveTv.google_analytics_configured? %>
<p class="muted">ID misurazione attivo sul sito: <code><%= MatchLiveTv.google_analytics_measurement_id %></code></p>
<% else %>
<p class="muted">Google Analytics è configurato solo quando il titolare imposta lID misurazione sul server.</p>
<% end %>
<table class="legal-table">
<thead>
<tr><th>Nome</th><th>Finalità</th><th>Durata</th><th>Fornitore</th></tr>
</thead>
<tbody>
<tr>
<td><code>_ga</code></td>
<td>Distingue gli utenti (statistiche)</td>
<td>2 anni</td>
<td>Google</td>
</tr>
<tr>
<td><code>_ga_*</code></td>
<td>Mantiene lo stato della sessione Analytics</td>
<td>2 anni</td>
<td>Google</td>
</tr>
<tr>
<td><code>_gid</code></td>
<td>Distingue gli utenti (statistiche)</td>
<td>24 ore</td>
<td>Google</td>
</tr>
</tbody>
</table>
<p>
Puoi revocare il consenso dal banner o dalle impostazioni del browser.
Informazioni Google:
<a href="https://policies.google.com/privacy" target="_blank" rel="noopener noreferrer">Privacy Policy Google</a>,
<a href="https://tools.google.com/dlpage/gaoptout" target="_blank" rel="noopener noreferrer">componente opt-out Analytics</a>.
</p>
<h3>4.3 Cookie di terze parti durante il pagamento</h3>
<p>
Se acquisti un piano Premium, vieni reindirizzato a <strong>Stripe</strong> (checkout sicuro).
Stripe può impostare cookie propri sul dominio <code>stripe.com</code> per prevenire frodi e completare il pagamento.
Non controlliamo direttamente tali cookie: consulta la
<a href="https://stripe.com/privacy" target="_blank" rel="noopener noreferrer">privacy policy di Stripe</a>.
</p>
<h3>4.4 YouTube e servizi collegati</h3>
<p>
Lintegrazione YouTube per le dirette avviene tramite API lato server; il sito pubblico non incorpora
player YouTube con cookie di profilazione. I visitatori che aprono un link YouTube esterno
sono soggetti alle policy di Google/YouTube su quel dominio.
</p>
</section>
<section>
<h2>5. Come disabilitare i cookie dal browser</h2>
<p>
Puoi bloccare o cancellare i cookie dalle impostazioni del browser (Chrome, Firefox, Safari, Edge).
Disabilitando i cookie necessari alcune funzioni (es. login) potrebbero non funzionare.
</p>
</section>
<section>
<h2>6. Diritti e contatti</h2>
<p>
Per esercitare i diritti previsti dal GDPR (accesso, cancellazione, opposizione, revoca consenso)
scrivi a <a href="mailto:<%= MatchLiveTv.privacy_controller_email %>"><%= MatchLiveTv.privacy_controller_email %></a>.
Dettagli nel <%= link_to "documento privacy", public_privacy_path %>.
</p>
</section>
<p style="margin-top:24px">
<button type="button" class="btn btn-secondary" data-cookie-manage>Gestisci preferenze cookie</button>
</p>
</div>

View File

@@ -171,8 +171,12 @@
<section>
<h2>11. Cookie e tecnologie simili</h2>
<p>
Il sito utilizza cookie tecnici necessari al funzionamento (es. sessione di login) e, in futuro,
eventuali strumenti di analisi solo previo consenso ove richiesto. Per dettagli aggiornati contatta il titolare.
Il sito utilizza cookie tecnici necessari (sessione di login, sicurezza, memorizzazione delle preferenze cookie)
e, previo consenso tramite il banner, <strong>Google Analytics</strong> per statistiche aggregate.
</p>
<p>
Puoi gestire le preferenze in qualsiasi momento dal link «Gestisci cookie» nel footer
o consultare la <%= link_to "Cookie policy", public_cookies_path %>.
</p>
</section>

View File

@@ -0,0 +1,21 @@
<div id="cookie-banner" class="cookie-banner" role="dialog" aria-labelledby="cookie-banner-title" aria-describedby="cookie-banner-desc" hidden>
<div class="cookie-banner__inner">
<div class="cookie-banner__text">
<p id="cookie-banner-title" class="cookie-banner__title">Cookie e privacy</p>
<p id="cookie-banner-desc" class="cookie-banner__desc">
Usiamo cookie necessari per login e sicurezza. Con il tuo consenso attiviamo anche
<strong>Google Analytics</strong> per statistiche aggregate sul sito.
<%= link_to "Cookie policy", public_cookies_path, class: "cookie-banner__link" %>
e <%= link_to "Privacy", public_privacy_path, class: "cookie-banner__link" %>.
</p>
</div>
<div class="cookie-banner__actions">
<button type="button" class="btn btn-secondary cookie-banner__btn" data-cookie-reject>
Solo necessari
</button>
<button type="button" class="btn btn-primary cookie-banner__btn" data-cookie-accept-all>
Accetta tutti
</button>
</div>
</div>
</div>

View File

@@ -8,7 +8,9 @@
<%= link_to "Dirette", public_live_index_path %> ·
<%= link_to "FAQ", public_faq_path %> ·
<%= link_to "Privacy", public_privacy_path %> ·
<%= link_to "Cookie", public_cookies_path %> ·
<%= link_to "Termini", public_termini_path %>
· <button type="button" class="footer-link-btn" data-cookie-manage>Gestisci cookie</button>
</div>
<div class="site-footer__legal">
<p>© 2026 Emiliano Frascaro P. IVA 14230270960</p>

View File

@@ -140,5 +140,13 @@ module MatchLiveTv
def replay_download_url_ttl_seconds
ENV.fetch("REPLAY_DOWNLOAD_URL_TTL_SECONDS", "900").to_i
end
def google_analytics_measurement_id
ENV["GOOGLE_ANALYTICS_MEASUREMENT_ID"].presence
end
def google_analytics_configured?
google_analytics_measurement_id.present?
end
end
end

View File

@@ -116,6 +116,8 @@ Rails.application.routes.draw do
get "prezzi", to: "pages#pricing", as: :prezzi
get "pricing", to: redirect("/prezzi")
get "privacy", to: "pages#privacy", as: :privacy
get "cookie", to: "pages#cookies", as: :cookies
get "cookies", to: redirect("/cookie")
get "termini", to: "pages#terms", as: :termini
get "signup", to: "registrations#new"
post "signup", to: "registrations#create"

View File

@@ -0,0 +1,134 @@
(function () {
"use strict";
var STORAGE_KEY = "mltv_cookie_consent";
var COOKIE_NAME = "mltv_cookie_consent";
var COOKIE_MAX_AGE = 365 * 24 * 60 * 60;
function readConsent() {
try {
var raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
return JSON.parse(raw);
} catch (e) {
return null;
}
}
function writeConsent(consent) {
var payload = {
necessary: true,
analytics: !!consent.analytics,
ts: Date.now()
};
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(payload));
} catch (e) { /* ignore */ }
var value = payload.analytics ? "all" : "necessary";
document.cookie =
COOKIE_NAME + "=" + value +
"; path=/; max-age=" + COOKIE_MAX_AGE +
"; SameSite=Lax" +
(location.protocol === "https:" ? "; Secure" : "");
return payload;
}
function measurementId() {
return document.body && document.body.getAttribute("data-ga-id");
}
function loadGoogleAnalytics() {
var id = measurementId();
if (!id || window.__mltvGaLoaded) return;
window.dataLayer = window.dataLayer || [];
function gtag() { window.dataLayer.push(arguments); }
window.gtag = gtag;
gtag("consent", "default", {
analytics_storage: "denied",
ad_storage: "denied",
ad_user_data: "denied",
ad_personalization: "denied",
functionality_storage: "granted",
security_storage: "granted"
});
gtag("consent", "update", { analytics_storage: "granted" });
var script = document.createElement("script");
script.async = true;
script.src = "https://www.googletagmanager.com/gtag/js?id=" + encodeURIComponent(id);
script.onload = function () {
gtag("js", new Date());
gtag("config", id, { anonymize_ip: true, allow_google_signals: false });
};
document.head.appendChild(script);
window.__mltvGaLoaded = true;
}
function applyConsent(consent) {
if (consent && consent.analytics) {
loadGoogleAnalytics();
}
}
function hideBanner(banner) {
if (!banner) return;
banner.hidden = true;
banner.setAttribute("aria-hidden", "true");
}
function showBanner(banner) {
if (!banner) return;
banner.hidden = false;
banner.removeAttribute("aria-hidden");
}
function bindBanner(banner) {
var acceptAll = banner.querySelector("[data-cookie-accept-all]");
var reject = banner.querySelector("[data-cookie-reject]");
if (acceptAll) {
acceptAll.addEventListener("click", function () {
var c = writeConsent({ analytics: true });
applyConsent(c);
hideBanner(banner);
});
}
if (reject) {
reject.addEventListener("click", function () {
writeConsent({ analytics: false });
hideBanner(banner);
});
}
}
function init() {
var banner = document.getElementById("cookie-banner");
var consent = readConsent();
if (consent) {
applyConsent(consent);
hideBanner(banner);
} else {
showBanner(banner);
}
bindBanner(banner);
document.querySelectorAll("[data-cookie-manage]").forEach(function (el) {
el.addEventListener("click", function (e) {
e.preventDefault();
showBanner(banner);
});
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();

View File

@@ -1060,6 +1060,7 @@ body.nav-menu-open { overflow: hidden; }
.legal-doc { max-width: 760px; padding-top: 24px; padding-bottom: 48px; color: #bbb; line-height: 1.65; font-size: 0.95rem; }
.legal-doc h1 { color: #fff; font-size: 1.6rem; margin-bottom: 8px; }
.legal-doc h2 { color: #fff; font-size: 1.1rem; margin: 28px 0 10px; }
.legal-doc h3 { color: #ddd; font-size: 1rem; margin: 20px 0 8px; }
.legal-doc p { margin: 0 0 12px; }
.legal-doc ul { margin: 0 0 14px; padding-left: 1.25rem; }
.legal-doc a { color: #e53935; }
@@ -1272,3 +1273,84 @@ label { display: block; margin-bottom: 6px; font-size: 0.9rem; color: #c8c8d4; f
.plan-change-info__box li + li {
margin-top: 8px;
}
/* Cookie banner (GDPR / ePrivacy) */
.cookie-banner {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
padding: 16px;
background: rgba(10, 10, 14, 0.96);
border-top: 1px solid #2a2a36;
box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.45);
}
.cookie-banner[hidden] {
display: none !important;
}
.cookie-banner__inner {
max-width: 1100px;
margin: 0 auto;
display: flex;
flex-wrap: wrap;
gap: 16px 24px;
align-items: center;
justify-content: space-between;
}
.cookie-banner__text {
flex: 1 1 280px;
min-width: 0;
}
.cookie-banner__title {
margin: 0 0 6px;
font-weight: 700;
color: #fff;
font-size: 1rem;
}
.cookie-banner__desc {
margin: 0;
color: #aaa;
font-size: 0.88rem;
line-height: 1.5;
}
.cookie-banner__link {
color: #e53935;
text-decoration: underline;
}
.cookie-banner__actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
flex-shrink: 0;
}
.cookie-banner__btn {
white-space: nowrap;
}
.footer-link-btn {
background: none;
border: none;
padding: 0;
font: inherit;
color: #ff6b6b;
cursor: pointer;
text-decoration: none;
}
.footer-link-btn:hover {
color: #e53935;
text-decoration: underline;
}
body.cookie-banner-visible {
padding-bottom: 120px;
}

BIN
brand/CopertinaCanale.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
brand/CopertinaCanale_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
brand/CopertinaCanale_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
brand/CopertinaCanale_4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
brand/CopertinaCanale_5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
brand/CopertinaCanale_6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@@ -15,6 +15,9 @@ PRIVACY_CONTACT_EMAIL=privacy@matchlive.it
MAILER_FROM=Match Live TV <noreply@matchlive.it>
PASSWORD_RESET_EXPIRY_HOURS=2
# Google Analytics 4 (opzionale — caricato solo dopo consenso cookie «Accetta tutti»)
# GOOGLE_ANALYTICS_MEASUREMENT_ID=G-XXXXXXXXXX
# Stripe (test: sk_test_... / price_... da Dashboard modalità Test)
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=

View File

@@ -101,6 +101,7 @@ services:
REPLAY_STORAGE_ACCESS_KEY_ID: ${REPLAY_STORAGE_ACCESS_KEY_ID:-}
REPLAY_STORAGE_SECRET_ACCESS_KEY: ${REPLAY_STORAGE_SECRET_ACCESS_KEY:-}
REPLAY_STORAGE_FORCE_PATH_STYLE: ${REPLAY_STORAGE_FORCE_PATH_STYLE:-true}
GOOGLE_ANALYTICS_MEASUREMENT_ID: ${GOOGLE_ANALYTICS_MEASUREMENT_ID:-}
ports:
- "3000:3000" # Solo LAN — NPM proxy HTTPS
depends_on: