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

@@ -22,5 +22,22 @@ module Admin
Rails.logger.error("[admin] stop session #{@session.id}: #{e.class} #{e.message}")
redirect_to admin_session_path(@session), alert: "Errore durante la chiusura: #{e.message}"
end
def regia_link
@session = StreamSession.find(params[:id])
if @session.terminal?
redirect_to admin_session_path(@session), alert: "Sessione terminata: impossibile generare il link regia."
return
end
access = Sessions::RegiaAccess.new(@session)
token = access.issue_token!
redirect_to admin_session_path(@session),
notice: "Link regia generato.",
flash: {
regia_url: access.url(token),
regia_expires_at: @session.regia_token_expires_at&.iso8601
}
end
end
end

View File

@@ -1,6 +1,9 @@
module Api
module V1
class MatchesController < BaseController
include Api::UrlHelper
include BrandingAttachments
before_action :set_team, only: %i[index create]
before_action :set_match, only: %i[show update destroy]
@@ -14,6 +17,7 @@ module Api
def create
match = @team.matches.create!(match_params)
attach_opponent_logo(match)
render json: match_json(match), status: :created
end
@@ -24,7 +28,9 @@ module Api
def update
attrs = match_params.to_h
normalize_scoring_rules!(attrs)
normalize_opponent_color!(attrs)
@match.update!(attrs)
attach_opponent_logo(@match)
render json: match_json(@match)
end
@@ -49,18 +55,17 @@ module Api
def set_match
team_ids = current_user.streamable_teams.map(&:id)
@match = Match.where(team_id: team_ids).find(params[:id])
@match = Match.includes(:team).where(team_id: team_ids).find(params[:id])
end
def match_params
params.require(:match).permit(
:opponent_name, :location, :scheduled_at, :sport, :sets_to_win,
:category,
:category, :opponent_primary_color,
scoring_rules: %i[points_per_set points_deciding_set min_point_lead]
)
end
# Regolamento standard: scoring_rules assente o {} → default FIPAV lato Scoring::Rules.
def normalize_scoring_rules!(attrs)
return unless attrs.key?("scoring_rules")
@@ -68,12 +73,27 @@ module Api
attrs["scoring_rules"] = {} if rules.blank?
end
def normalize_opponent_color!(attrs)
return unless attrs.key?("opponent_primary_color")
attrs["opponent_primary_color"] = normalize_hex_color(
attrs["opponent_primary_color"],
Match::DEFAULT_OPPONENT_COLOR
)
end
def attach_opponent_logo(match)
file = params[:opponent_logo_file]
match.opponent_logo_file.attach(file) if file.present?
end
def match_json(match, detail: false)
active = active_session_for(match)
team = match.team
{
id: match.id,
team_id: match.team_id,
team_name: match.team.name,
team_name: team.name,
opponent_name: match.opponent_name,
location: match.location,
scheduled_at: match.scheduled_at,
@@ -81,6 +101,11 @@ module Api
sets_to_win: match.sets_to_win,
scoring_rules: match.scoring_rules.presence,
category: match.category,
home_primary_color: team.effective_primary_color,
home_secondary_color: team.effective_secondary_color,
home_logo_url: api_absolute_url(team.effective_logo_url),
opponent_primary_color: match.effective_opponent_primary_color,
opponent_logo_url: api_absolute_url(match.opponent_logo_url),
active_session_id: active&.id,
active_session_status: active&.status,
stream_completed: match.stream_completed?,

View File

@@ -100,13 +100,31 @@ module Api
end
def network_test
upload_mbps = params[:upload_mbps].to_f
preset = Sessions::SelectQuality.call(upload_mbps: upload_mbps)
@session.update!(
quality_preset: preset[:id],
target_bitrate: preset[:target_bitrate],
target_fps: preset[:target_fps]
)
@session.stream_events.create!(
event_type: "network_test",
metadata: params.permit(:download_mbps, :upload_mbps, :latency_ms, :network_type).to_h,
metadata: params.permit(:download_mbps, :upload_mbps, :latency_ms, :network_type)
.to_h.merge(
"quality_preset" => preset[:id],
"target_bitrate" => preset[:target_bitrate]
),
occurred_at: Time.current
)
ready = params[:upload_mbps].to_f >= (@session.target_bitrate / 1_000_000.0 * 0.8)
render json: { ready: ready, target_upload_mbps: @session.target_bitrate / 1_000_000.0 }
target_upload = preset[:target_bitrate] / 1_000_000.0
ready = upload_mbps >= (target_upload * Sessions::SelectQuality::UPLOAD_HEADROOM)
render json: {
ready: ready,
target_upload_mbps: target_upload,
quality_preset: preset[:id],
target_bitrate: preset[:target_bitrate],
target_fps: preset[:target_fps]
}
end
def youtube_stats

View File

@@ -1,6 +1,9 @@
module Api
module V1
class TeamsController < BaseController
include Api::UrlHelper
include BrandingAttachments
def index
teams = current_user.streamable_teams
render json: teams.map { |t| team_json(t) }
@@ -48,6 +51,7 @@ module Api
def update
team = current_user.manageable_teams.find(params[:id])
team.update!(team_params)
attach_team_logo(team)
render json: team_json(team)
end
@@ -69,7 +73,19 @@ module Api
private
def team_params
params.require(:team).permit(:name, :sport, :logo_url)
p = params.require(:team).permit(:name, :sport, :logo_url, :primary_color, :secondary_color)
if p[:primary_color].present?
p[:primary_color] = normalize_hex_color(p[:primary_color], p[:primary_color])
end
if p[:secondary_color].present?
p[:secondary_color] = normalize_hex_color(p[:secondary_color], p[:secondary_color])
end
p
end
def attach_team_logo(team)
file = params[:logo_file]
team.logo_file.attach(file) if file.present?
end
def team_json(team, detail: false)
@@ -80,7 +96,7 @@ module Api
id: team.id,
name: team.name,
sport: team.sport,
logo_url: team.effective_logo_url,
logo_url: api_absolute_url(team.effective_logo_url),
primary_color: team.effective_primary_color,
secondary_color: team.effective_secondary_color,
club_id: team.club_id,

View File

@@ -0,0 +1,16 @@
module Api
module UrlHelper
extend ActiveSupport::Concern
private
def api_absolute_url(path_or_url)
return nil if path_or_url.blank?
return path_or_url if path_or_url.match?(/\Ahttps?:\/\//i)
base = request.base_url
path = path_or_url.start_with?("/") ? path_or_url : "/#{path_or_url}"
"#{base}#{path}"
end
end
end

View File

@@ -6,7 +6,7 @@ module Public
layout "regia"
protect_from_forgery with: :null_session
skip_before_action :verify_authenticity_token, only: %i[score pause stop]
skip_before_action :verify_authenticity_token, only: %i[score pause resume stop]
before_action :set_session_from_token
@@ -44,6 +44,13 @@ module Public
render json: status_payload
end
def resume
Sessions::Resume.new(@session).call
render json: status_payload
rescue Teams::EntitlementError => e
render json: status_payload.merge(error: e.message), status: :unprocessable_entity
end
def stop
Sessions::Stop.new(@session).call
render json: status_payload
@@ -55,8 +62,7 @@ module Public
@session = Sessions::RegiaAccess.find_session_by_token(params[:token])
return if @session
render file: Rails.root.join("public/404.html"), status: :not_found, layout: false
throw :abort
head :not_found
end
def status_payload