Stabilizza regia, live web e sync punteggi app nativa.

Corregge scadenza token regia oltre le 8h, tabellone HTML sulla pagina live,
pulsanti pausa/ripresa in regia, link admin e broadcast ActionCable diretto.
App Android: overlay branding, sync score da regia via WebSocket con reconnect e poll.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-08 09:30:43 +02:00
parent 08e800120a
commit ae36d17adb
55 changed files with 2819 additions and 285 deletions

View File

@@ -1,7 +1,7 @@
<% club_name = @match.team.club&.name %>
<% content_for :title do %><%= club_name %> · <%= @match.team.name %> vs <%= @match.opponent_name %> — Diretta live<% end %>
<% content_for :meta_description do %>Segui in diretta live <%= club_name %> — <%= @match.team.name %> vs <%= @match.opponent_name %><% if @match.location.present? %> — <%= @match.location %><% end %>. Guarda dal browser senza installare app.<% end %>
<% content_for :robots, "noindex, follow" %>
<% content_for :robots, @session.publicly_listed? ? "index, follow" : "noindex, nofollow" %>
<% content_for :head do %>
<% unless @stream_closed || @session.platform == "youtube" %>
@@ -41,7 +41,7 @@
<% else %>
<div class="live-player-wrap">
<video id="player" controls playsinline muted autoplay poster="/images/copertina-canale.png"></video>
<%# Tabellone e watermark sono bruciati nel video dall'app mobile (overlay client-side). %>
<%= render "public/live/scorebug", match: @match, session: @session %>
<button type="button" id="play-hint" class="live-play-hint" hidden>
▶ Avvia la diretta
</button>
@@ -58,6 +58,88 @@
const pausedMsg = document.getElementById("paused-msg");
const awaitingMsg = document.getElementById("awaiting-msg");
const playHint = document.getElementById("play-hint");
const scorebug = document.getElementById("live-scorebug");
const streamBadge = document.getElementById("live-stream-badge");
const homeName = "<%= j @match.team.name %>";
const awayName = "<%= j @match.opponent_name %>";
function abbrevName(name, max = 16) {
const n = String(name || "");
return n.length <= max ? n : `${n.slice(0, max - 1)}…`;
}
function scoreColumnsFromPayload(score) {
if (!score) return { labels: ["1"], home: ["0"], away: ["0"], liveIndex: 0 };
const partials = score.set_partials || score.setPartials || [];
const labels = partials.map((p) => String(p.set ?? p["set"] ?? ""));
const home = partials.map((p) => String(p.home ?? p["home"] ?? "0"));
const away = partials.map((p) => String(p.away ?? p["away"] ?? "0"));
const currentSet = score.current_set ?? score.currentSet ?? 1;
labels.push(String(currentSet));
home.push(String(score.home_points ?? score.homePoints ?? 0));
away.push(String(score.away_points ?? score.awayPoints ?? 0));
return { labels, home, away, liveIndex: labels.length - 1 };
}
function renderLiveScorebug(score) {
if (!scorebug) return;
const cols = scoreColumnsFromPayload(score);
const homeLabel = abbrevName(scorebug.dataset.homeName || homeName);
const awayLabel = abbrevName(scorebug.dataset.awayName || awayName);
const headCells = cols.labels
.map((label) => `<th class="live-scorebug__col-set live-scorebug__col-h" scope="col">${label}</th>`)
.join("");
const homeCells = cols.home
.map((val, i) => {
const live = i === cols.liveIndex ? " live-scorebug__pts--live-home" : "";
return `<td class="live-scorebug__pts${live}">${val}</td>`;
})
.join("");
const awayCells = cols.away
.map((val, i) => {
const live = i === cols.liveIndex ? " live-scorebug__pts--live-away" : "";
return `<td class="live-scorebug__pts${live}">${val}</td>`;
})
.join("");
scorebug.innerHTML = `
<table class="live-scorebug__table" aria-label="Tabellone">
<caption class="visually-hidden">Punteggio live</caption>
<thead><tr>
<th class="live-scorebug__corner" scope="col">Squadra</th>${headCells}
</tr></thead>
<tbody>
<tr class="live-scorebug__row--home">
<th class="live-scorebug__team" scope="row">${homeLabel}</th>${homeCells}
</tr>
<tr class="live-scorebug__row--away">
<th class="live-scorebug__team" scope="row">${awayLabel}</th>${awayCells}
</tr>
</tbody>
</table>
<div class="live-scorebug__brand" aria-hidden="true">
<span class="live-scorebug__brand-text">MATCH <span class="live-scorebug__brand-accent">LIVE TV</span></span>
</div>`;
}
function syncStreamBadge(data) {
if (!streamBadge) return;
if (data.stream_closed) {
streamBadge.textContent = "Terminata";
streamBadge.className = "live-ovl-badge live-ovl-badge--right badge-ended";
return;
}
const paused = !!(data.paused || data.status === "paused");
if (paused) {
streamBadge.textContent = "In pausa";
streamBadge.className = "live-ovl-badge live-ovl-badge--right badge-wait";
} else if (data.on_air) {
streamBadge.textContent = "LIVE";
streamBadge.className = "live-ovl-badge live-ovl-badge--right badge-live";
} else {
streamBadge.textContent = "In attesa";
streamBadge.className = "live-ovl-badge live-ovl-badge--right badge-wait";
}
}
let hls = null;
let playerStarted = false;
@@ -247,6 +329,9 @@
return;
}
renderLiveScorebug(data.score);
syncStreamBadge(data);
const onAir = !!data.on_air;
const sessionLive = !!data.live;