Migliora overlay basket Android, regia e catalogo sport senza timer.

Fix crash e race su +3, layout tabellone con squadre ai lati, safe area in diretta e rimozione overlay cronometro; backend con sync punteggio periodo, link diretta in regia e init score_state basket.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-09 23:31:12 +02:00
parent 34c97f4447
commit 2b7e8b9e36
23 changed files with 341 additions and 167 deletions

View File

@@ -167,6 +167,7 @@ module Api
def score_sync_params
params.permit(
:home_sets, :away_sets, :home_points, :away_points, :current_set,
:clock_secs, :clock_running, :period, :home_score, :away_score, :overtime,
set_partials: %i[set home away]
)
end

View File

@@ -18,8 +18,10 @@ module Public
@rules = Scoring::Rules.from_match(@match)
@stream_closed = @session.terminal?
@on_air = !@stream_closed && mediamtx_stream_playable?(@session)
@share_url = request.original_url
@share_title = "Regia — #{@team.name} vs #{@match.opponent_name}"
@regia_share_url = request.original_url
@regia_share_title = "Regia — #{@team.name} vs #{@match.opponent_name}"
@live_share_url = @session.share_url
@live_share_title = "#{@team.name} vs #{@match.opponent_name} — Diretta Match Live TV"
end
def status

View File

@@ -2,7 +2,7 @@
module Sports
class OverlayKind
ALL = %w[none timer volley basket timed racket].freeze
ALL = %w[none volley basket timed racket].freeze
def self.valid?(value)
ALL.include?(value.to_s)

View File

@@ -99,6 +99,7 @@ class Match < ApplicationRecord
def effective_overlay_kind
kind = overlay_kind.presence || Sports::Catalog.overlay_for(sport_key)
kind = "none" if kind == "timer"
allowed = Sports::Catalog.allowed_overlays_for(sport_key)
allowed.include?(kind) ? kind : Sports::Catalog.overlay_for(sport_key)
end

View File

@@ -35,7 +35,7 @@ module Sessions
StreamSession.transaction do
session.save!
session.create_score_state!
Scoring::Engine.ensure_score_for(session)
mtx = Mediamtx::Client.new
mtx.create_path(session)
log_event(session, "pairing", { created: true, platform: session.platform })

View File

@@ -19,8 +19,10 @@
data-hls-url="<%= j @session.hls_playback_url %>"
data-home-name="<%= j @team.name %>"
data-away-name="<%= j @match.opponent_name %>"
data-share-url="<%= j @share_url %>"
data-share-title="<%= j @share_title %>"
data-regia-share-url="<%= j @regia_share_url %>"
data-regia-share-title="<%= j @regia_share_title %>"
data-live-share-url="<%= j @live_share_url %>"
data-live-share-title="<%= j @live_share_title %>"
data-points-target="<%= @match.effective_board_type.in?(%w[volley racket]) ? Scoring::Rules.from_match(@match).points_target(@score.current_set) : 0 %>"
data-stream-closed="<%= @stream_closed %>"
data-initial-paused="<%= @session.paused? %>">
@@ -33,13 +35,30 @@
</span>
</header>
<div class="regia-share">
<button type="button" class="regia-btn regia-btn--outline" id="btn-share" style="margin:0">
Condividi link regia
</button>
<button type="button" class="regia-btn regia-btn--outline" id="btn-copy" style="margin:0">
Copia link
</button>
<% if @live_share_url.present? %>
<div class="regia-share-group">
<p class="regia-share-label">Link diretta (spettatori)</p>
<div class="regia-share">
<button type="button" class="regia-btn regia-btn--outline" id="btn-share-live" style="margin:0">
Condividi diretta
</button>
<button type="button" class="regia-btn regia-btn--outline" id="btn-copy-live" style="margin:0">
Copia link
</button>
</div>
</div>
<% end %>
<div class="regia-share-group">
<p class="regia-share-label">Link regia (punteggio)</p>
<div class="regia-share">
<button type="button" class="regia-btn regia-btn--outline" id="btn-share-regia" style="margin:0">
Condividi link regia
</button>
<button type="button" class="regia-btn regia-btn--outline" id="btn-copy-regia" style="margin:0">
Copia link
</button>
</div>
</div>
<div class="regia-preview">
@@ -74,4 +93,4 @@
</div>
</div>
<script src="/regia.js?v=5"></script>
<script src="/regia.js?v=6"></script>

View File

@@ -1,12 +1,12 @@
# Catalogo sport Match Live TV — board e overlay impliciti, regole standard.
# board: volley | basket | timed | racket | timer | generic
# overlay: none | timer | volley | basket | timed | racket
# overlay: none | volley | basket | timed | racket
pallavolo:
label: Pallavolo
board: volley
overlay: volley
allowed_overlays: [volley, timer, none]
allowed_overlays: [volley, none]
default_rules:
sets_to_win: 3
points_per_set: 25
@@ -17,7 +17,7 @@ beach_volley:
label: Beach Volley
board: volley
overlay: volley
allowed_overlays: [volley, timer, none]
allowed_overlays: [volley, none]
default_rules:
sets_to_win: 2
points_per_set: 21
@@ -28,7 +28,7 @@ sitting_volley:
label: Sitting Volley
board: volley
overlay: volley
allowed_overlays: [volley, timer, none]
allowed_overlays: [volley, none]
default_rules:
sets_to_win: 3
points_per_set: 25
@@ -39,7 +39,7 @@ basket:
label: Basket
board: basket
overlay: basket
allowed_overlays: [basket, timer, none]
allowed_overlays: [basket, none]
default_rules:
periods: 4
period_duration_secs: 600
@@ -49,7 +49,7 @@ calcio_a_5:
label: Calcio a 5
board: timed
overlay: timed
allowed_overlays: [timed, timer, none]
allowed_overlays: [timed, none]
default_rules:
periods: 2
period_duration_secs: 1200
@@ -59,7 +59,7 @@ pallamano:
label: Pallamano
board: timed
overlay: timed
allowed_overlays: [timed, timer, none]
allowed_overlays: [timed, none]
default_rules:
periods: 2
period_duration_secs: 1800
@@ -69,7 +69,7 @@ hockey_indoor:
label: Hockey Indoor
board: timed
overlay: timed
allowed_overlays: [timed, timer, none]
allowed_overlays: [timed, none]
default_rules:
periods: 2
period_duration_secs: 1200
@@ -79,7 +79,7 @@ floorball:
label: Floorball
board: timed
overlay: timed
allowed_overlays: [timed, timer, none]
allowed_overlays: [timed, none]
default_rules:
periods: 3
period_duration_secs: 1200
@@ -89,7 +89,7 @@ tennis:
label: Tennis
board: racket
overlay: racket
allowed_overlays: [racket, timer, none]
allowed_overlays: [racket, none]
default_rules:
sets_to_win: 2
points_per_set: 6
@@ -100,7 +100,7 @@ tennis_tavolo:
label: Tennis Tavolo
board: racket
overlay: racket
allowed_overlays: [racket, timer, none]
allowed_overlays: [racket, none]
default_rules:
sets_to_win: 3
points_per_set: 11
@@ -111,7 +111,7 @@ badminton:
label: Badminton
board: racket
overlay: racket
allowed_overlays: [racket, timer, none]
allowed_overlays: [racket, none]
default_rules:
sets_to_win: 2
points_per_set: 21
@@ -122,7 +122,7 @@ padel:
label: Padel
board: racket
overlay: racket
allowed_overlays: [racket, timer, none]
allowed_overlays: [racket, none]
default_rules:
sets_to_win: 2
points_per_set: 6
@@ -133,7 +133,7 @@ pickleball:
label: Pickleball
board: racket
overlay: racket
allowed_overlays: [racket, timer, none]
allowed_overlays: [racket, none]
default_rules:
sets_to_win: 2
points_per_set: 11
@@ -143,41 +143,41 @@ pickleball:
ginnastica_artistica:
label: Ginnastica Artistica
board: timer
overlay: timer
allowed_overlays: [timer, none]
overlay: none
allowed_overlays: [none]
default_rules: {}
ginnastica_ritmica:
label: Ginnastica Ritmica
board: timer
overlay: timer
allowed_overlays: [timer, none]
overlay: none
allowed_overlays: [none]
default_rules: {}
danza_sportiva:
label: Danza Sportiva
board: timer
overlay: timer
allowed_overlays: [timer, none]
overlay: none
allowed_overlays: [none]
default_rules: {}
cheerleading:
label: Cheerleading
board: timer
overlay: timer
allowed_overlays: [timer, none]
overlay: none
allowed_overlays: [none]
default_rules: {}
pattinaggio:
label: Pattinaggio
board: timer
overlay: timer
allowed_overlays: [timer, none]
overlay: none
allowed_overlays: [none]
default_rules: {}
altro:
label: Altro
board: generic
overlay: none
allowed_overlays: [none, timer]
allowed_overlays: [none]
default_rules: {}

View File

@@ -173,10 +173,22 @@
margin-top: 8px;
}
.regia-share-group {
margin: 12px 0;
}
.regia-share-label {
margin: 0 0 6px;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--regia-muted);
}
.regia-share {
display: flex;
gap: 8px;
margin: 12px 0;
}
.regia-share .regia-btn { flex: 1; font-size: 0.9rem; padding: 12px; }

View File

@@ -14,8 +14,10 @@
hlsUrl: root.dataset.hlsUrl,
homeName: root.dataset.homeName,
awayName: root.dataset.awayName,
shareUrl: root.dataset.shareUrl,
shareTitle: root.dataset.shareTitle,
regiaShareUrl: root.dataset.regiaShareUrl,
regiaShareTitle: root.dataset.regiaShareTitle,
liveShareUrl: root.dataset.liveShareUrl,
liveShareTitle: root.dataset.liveShareTitle,
streamClosed: root.dataset.streamClosed === "true"
};
@@ -360,12 +362,12 @@
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() {
const payload = {
title: cfg.shareTitle,
text: "Apri questo link per gestire il punteggio della diretta:",
url: cfg.shareUrl
};
async function shareLink(url, title, text) {
if (!url) {
toast("Link non disponibile");
return;
}
const payload = { title, text, url };
if (navigator.share) {
try {
await navigator.share(payload);
@@ -374,20 +376,38 @@
if (err.name === "AbortError") return;
}
}
await copyLink();
await copyLink(url);
}
async function copyLink() {
async function copyLink(url) {
if (!url) {
toast("Link non disponibile");
return;
}
try {
await navigator.clipboard.writeText(cfg.shareUrl);
await navigator.clipboard.writeText(url);
toast("Link copiato");
} catch (_) {
prompt("Copia il link:", cfg.shareUrl);
prompt("Copia il link:", url);
}
}
document.getElementById("btn-share")?.addEventListener("click", () => shareLink());
document.getElementById("btn-copy")?.addEventListener("click", () => copyLink());
document.getElementById("btn-share-regia")?.addEventListener("click", () =>
shareLink(
cfg.regiaShareUrl,
cfg.regiaShareTitle,
"Apri questo link per gestire il punteggio della diretta:"
)
);
document.getElementById("btn-copy-regia")?.addEventListener("click", () => copyLink(cfg.regiaShareUrl));
document.getElementById("btn-share-live")?.addEventListener("click", () =>
shareLink(
cfg.liveShareUrl,
cfg.liveShareTitle,
"Guarda la diretta su Match Live TV:"
)
);
document.getElementById("btn-copy-live")?.addEventListener("click", () => copyLink(cfg.liveShareUrl));
function bindPreviewStallRecovery() {
if (!els.preview) return;

View File

@@ -14,6 +14,7 @@ RSpec.describe "Api::V1::Sports", type: :request do
expect(body).to be_an(Array)
pallavolo = body.find { |s| s["key"] == "pallavolo" }
expect(pallavolo["board"]).to eq("volley")
expect(pallavolo["allowed_overlays"]).to include("volley", "timer", "none")
expect(pallavolo["allowed_overlays"]).to include("volley", "none")
expect(pallavolo["allowed_overlays"]).not_to include("timer")
end
end

View File

@@ -11,7 +11,9 @@ RSpec.describe "Public regia", type: :request do
token = Sessions::RegiaAccess.new(session).issue_token!
get public_regia_path(token)
expect(response).to have_http_status(:ok)
expect(response.body).to include("Condividi diretta")
expect(response.body).to include("Condividi link regia")
expect(response.body).to include("/live/#{session.id}")
expect(response.body).not_to include("Aruba")
end