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

@@ -233,6 +233,90 @@ body.admin-body {
align-items: center;
}
.admin-links-panel {
margin: 1.25rem 0 1.5rem;
padding: 1rem 1.1rem;
background: var(--card-bg, #14141c);
border: 1px solid var(--card-border, #2a2a36);
border-radius: 8px;
}
.admin-links-panel h3 {
margin: 0 0 0.6rem;
font-size: 0.95rem;
}
.admin-links-panel h3 + h3 {
margin-top: 1.1rem;
}
.admin-link-list {
list-style: none;
margin: 0;
padding: 0;
}
.admin-link-list li,
.admin-link-generated {
display: flex;
flex-wrap: wrap;
gap: 0.5rem 0.75rem;
align-items: baseline;
margin-bottom: 0.45rem;
}
.admin-link-label {
min-width: 6.5rem;
color: var(--muted, #9ca3af);
font-size: 0.85rem;
}
.admin-link-url {
color: var(--red, #e53935);
word-break: break-all;
}
.admin-link-code {
display: block;
margin: 0.35rem 0 0.5rem;
padding: 0.45rem 0.6rem;
background: #0a0a0e;
border-radius: 6px;
font-size: 0.78rem;
word-break: break-all;
}
.admin-link-compact {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
}
.admin-link-chip {
display: inline-block;
padding: 0.15rem 0.45rem;
border-radius: 999px;
background: #2a2a36;
color: #eee;
font-size: 0.72rem;
text-decoration: none;
}
.admin-link-chip:hover {
background: #3a3a48;
color: #fff;
}
.admin-link-chip--regia {
background: #3d2a00;
color: #f9a825;
}
.admin-inline-form {
display: inline-block;
margin-top: 0.35rem;
}
.admin-input {
padding: 0.45rem 0.6rem;
border: 1px solid #444;

View File

@@ -7,6 +7,7 @@
statusUrl: root.dataset.statusUrl,
scoreUrl: root.dataset.scoreUrl,
pauseUrl: root.dataset.pauseUrl,
resumeUrl: root.dataset.resumeUrl,
stopUrl: root.dataset.stopUrl,
cableUrl: root.dataset.cableUrl,
hlsUrl: root.dataset.hlsUrl,
@@ -30,13 +31,15 @@
modalConfirm: document.getElementById("modal-confirm"),
modalCancel: document.getElementById("modal-cancel"),
preview: document.getElementById("regia-preview"),
previewPlaceholder: document.getElementById("regia-preview-placeholder")
previewPlaceholder: document.getElementById("regia-preview-placeholder"),
btnPause: document.getElementById("btn-pause")
};
let pendingAction = null;
let hls = null;
let previewStarted = false;
let wasPausedPreview = false;
let streamPaused = root.dataset.initialPaused === "true";
function hlsUrlFresh() {
const sep = cfg.hlsUrl.includes("?") ? "&" : "?";
@@ -108,7 +111,15 @@
}
}
function syncPauseButton(data) {
if (!els.btnPause) return;
const paused = !!(data.paused || data.status === "paused");
streamPaused = paused;
els.btnPause.textContent = paused ? "Riprendi diretta" : "Metti in pausa";
}
function setBadge(data) {
syncPauseButton(data);
if (data.stream_closed) {
els.badge.textContent = "Terminata";
els.badge.className = "regia-badge regia-badge--ended";
@@ -139,6 +150,8 @@
}
async function handleAction(action) {
const prevHome = els.homePoints?.textContent;
const prevAway = document.getElementById("away-points")?.textContent;
try {
const data = await postScore(action);
applyScorePayload(data);
@@ -152,6 +165,9 @@
openModal("Partita vinta", `${winner} ha vinto la partita. Chiudere la diretta?`, () => stopStream());
}
} catch (e) {
if (prevHome != null && els.homePoints) els.homePoints.textContent = prevHome;
const awayEl = document.getElementById("away-points");
if (prevAway != null && awayEl) awayEl.textContent = prevAway;
toast(e.message || "Errore");
}
}
@@ -284,10 +300,19 @@
showPreviewPlaceholder("Diretta terminata");
}
async function pauseStream() {
await fetch(cfg.pauseUrl, { method: "POST", headers: { Accept: "application/json" } });
toast("Diretta in pausa — copertina in onda");
pollStatus();
async function togglePauseStream() {
const url = streamPaused ? cfg.resumeUrl : cfg.pauseUrl;
const res = await fetch(url, { method: "POST", headers: { Accept: "application/json" } });
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.error || (streamPaused ? "Errore ripresa diretta" : "Errore pausa diretta"));
}
const data = await res.json();
const wasPaused = streamPaused;
applyScorePayload(data);
setBadge(data);
syncPreviewFromStatus(data);
toast(wasPaused ? "Diretta ripresa" : "Diretta in pausa — copertina in onda");
}
async function stopStream() {
@@ -302,7 +327,7 @@
return data;
}
document.getElementById("btn-pause")?.addEventListener("click", () => pauseStream().catch((e) => toast(e.message)));
els.btnPause?.addEventListener("click", () => togglePauseStream().catch((e) => toast(e.message)));
document.getElementById("btn-stop")?.addEventListener("click", () => stopStream().catch((e) => toast(e.message)));
async function shareLink() {