Allinea hub partite app nativa e API match al wizard semplificato.
Filtra le partite visibili lato client/server, corregge il parsing date Rails su Android, semplifica lo step Partita del wizard e restringe l'API ai campi effettivamente usati (campionato, set e scoring_rules). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -8,6 +8,7 @@ module Api
|
||||
matches = @team.matches
|
||||
.includes(:team, :stream_sessions)
|
||||
.order(Arel.sql("scheduled_at ASC NULLS LAST"), created_at: :desc)
|
||||
.select(&:coach_hub_visible?)
|
||||
render json: matches.map { |m| match_json(m) }
|
||||
end
|
||||
|
||||
@@ -21,7 +22,9 @@ module Api
|
||||
end
|
||||
|
||||
def update
|
||||
@match.update!(match_params)
|
||||
attrs = match_params.to_h
|
||||
normalize_scoring_rules!(attrs)
|
||||
@match.update!(attrs)
|
||||
render json: match_json(@match)
|
||||
end
|
||||
|
||||
@@ -52,11 +55,19 @@ module Api
|
||||
def match_params
|
||||
params.require(:match).permit(
|
||||
:opponent_name, :location, :scheduled_at, :sport, :sets_to_win,
|
||||
:category, :phase, roster_numbers: [],
|
||||
:category,
|
||||
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")
|
||||
|
||||
rules = attrs["scoring_rules"]
|
||||
attrs["scoring_rules"] = {} if rules.blank?
|
||||
end
|
||||
|
||||
def match_json(match, detail: false)
|
||||
active = active_session_for(match)
|
||||
{
|
||||
@@ -69,11 +80,11 @@ module Api
|
||||
sport: match.sport,
|
||||
sets_to_win: match.sets_to_win,
|
||||
scoring_rules: match.scoring_rules.presence,
|
||||
roster_numbers: match.roster_numbers,
|
||||
category: match.category,
|
||||
phase: match.phase,
|
||||
active_session_id: active&.id,
|
||||
active_session_status: active&.status
|
||||
active_session_status: active&.status,
|
||||
stream_completed: match.stream_completed?,
|
||||
coach_hub_visible: match.coach_hub_visible?
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ class Match < ApplicationRecord
|
||||
|
||||
validates :opponent_name, presence: true
|
||||
validates :sets_to_win, numericality: { greater_than: 0, less_than: 6 }
|
||||
validate :scoring_rules_shape, if: -> { scoring_rules.present? }
|
||||
|
||||
# category: campionato / descrizione torneo (facoltativo, es. "Serie C").
|
||||
|
||||
# Partite ancora da giocare in diretta (orario nel futuro).
|
||||
scope :scheduled_for_live, -> {
|
||||
@@ -55,4 +58,37 @@ class Match < ApplicationRecord
|
||||
def broadcast_active?
|
||||
stream_sessions.where(status: ACTIVE_SESSION_STATUSES).exists?
|
||||
end
|
||||
|
||||
def stream_completed?
|
||||
stream_sessions.where(status: %w[ended error]).exists?
|
||||
end
|
||||
|
||||
# Hub app coach: dirette da riprendere o partite programmate future.
|
||||
# Le bozze «Avversario» abbandonate (senza sessione) non compaiono.
|
||||
def coach_hub_visible?
|
||||
active = active_stream_session
|
||||
return true if active&.resumable?
|
||||
return true if active&.idle?
|
||||
|
||||
return false if stream_completed?
|
||||
|
||||
scheduled_upcoming?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def scoring_rules_shape
|
||||
rules = scoring_rules.is_a?(Hash) ? scoring_rules : {}
|
||||
%w[points_per_set points_deciding_set min_point_lead].each do |key|
|
||||
val = rules[key] || rules[key.to_sym]
|
||||
next if val.blank?
|
||||
|
||||
unless val.is_a?(Integer) || val.to_s.match?(/\A\d+\z/)
|
||||
errors.add(:scoring_rules, "#{key} non valido")
|
||||
next
|
||||
end
|
||||
int_val = val.to_i
|
||||
errors.add(:scoring_rules, "#{key} deve essere positivo") if int_val < 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
60
backend/lib/tasks/matches.rake
Normal file
60
backend/lib/tasks/matches.rake
Normal file
@@ -0,0 +1,60 @@
|
||||
namespace :matches do
|
||||
desc "Pulizia bozze abbandonate e sessioni idle obsolete (hub coach). DRY_RUN=1 per anteprima."
|
||||
task cleanup_stale: :environment do
|
||||
dry = ENV["DRY_RUN"] == "1"
|
||||
cutoff_abandoned = ENV.fetch("ABANDONED_DAYS", "1").to_i.days.ago
|
||||
cutoff_idle = ENV.fetch("IDLE_HOURS", "48").to_i.hours.ago
|
||||
|
||||
abandoned = Match
|
||||
.where(scheduled_at: nil)
|
||||
.where.missing(:stream_sessions)
|
||||
.where("matches.created_at < ?", cutoff_abandoned)
|
||||
|
||||
stale_idle_sessions = StreamSession
|
||||
.where(status: "idle")
|
||||
.where("stream_sessions.updated_at < ?", cutoff_idle)
|
||||
|
||||
puts "DRY RUN — nessuna modifica" if dry
|
||||
puts "Bozze abbandonate (senza sessione, > #{cutoff_abandoned}): #{abandoned.count}"
|
||||
abandoned.find_each do |match|
|
||||
label = "#{match.team.name} vs #{match.opponent_name} (#{match.id})"
|
||||
if dry
|
||||
puts " would delete match #{label}"
|
||||
else
|
||||
match.destroy!
|
||||
puts " deleted match #{label}"
|
||||
end
|
||||
end
|
||||
|
||||
puts "Sessioni idle obsolete (> #{cutoff_idle}): #{stale_idle_sessions.count}"
|
||||
stale_idle_sessions.find_each do |session|
|
||||
match = session.match
|
||||
label = "#{match.team.name} vs #{match.opponent_name} session=#{session.id}"
|
||||
if dry
|
||||
puts " would end idle session #{label}"
|
||||
else
|
||||
session.end_stream!
|
||||
puts " ended idle session #{label}"
|
||||
end
|
||||
end
|
||||
|
||||
hidden_completed = Match.includes(:stream_sessions).select do |m|
|
||||
m.stream_completed? && m.active_stream_session.nil?
|
||||
end
|
||||
puts "Partite concluse (già nascoste dall'hub): #{hidden_completed.size}"
|
||||
if ENV["DELETE_COMPLETED"] == "1"
|
||||
hidden_completed.each do |match|
|
||||
session_ids = match.stream_sessions.pluck(:id)
|
||||
next if Recording.where(stream_session_id: session_ids).exists?
|
||||
|
||||
label = "#{match.team.name} vs #{match.opponent_name} (#{match.id})"
|
||||
if dry
|
||||
puts " would delete completed #{label}"
|
||||
else
|
||||
match.destroy!
|
||||
puts " deleted completed #{label}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -5,6 +5,7 @@ RSpec.describe Match, "scheduling scopes" do
|
||||
Club.create!(name: "Sched Club", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff")
|
||||
end
|
||||
let!(:team) { club.teams.create!(name: "U16", sport: "volleyball") }
|
||||
let!(:user) { User.create!(email: "coach@test.com", name: "Coach", password: "password123", role: "coach") }
|
||||
|
||||
it "scheduled_for_live esclude partite già passate oggi" do
|
||||
past = team.matches.create!(opponent_name: "Past", scheduled_at: 2.hours.ago, sets_to_win: 3)
|
||||
@@ -13,4 +14,75 @@ RSpec.describe Match, "scheduling scopes" do
|
||||
expect(described_class.scheduled_for_live).to include(future)
|
||||
expect(described_class.scheduled_for_live).not_to include(past)
|
||||
end
|
||||
|
||||
describe "#coach_hub_visible?" do
|
||||
it "mostra diretta attiva da riprendere" do
|
||||
match = team.matches.create!(opponent_name: "Live", sets_to_win: 3)
|
||||
StreamSession.create!(match: match, user: user, platform: "matchlivetv", status: "paused")
|
||||
|
||||
expect(match.coach_hub_visible?).to be(true)
|
||||
end
|
||||
|
||||
it "mostra wizard in corso (sessione idle)" do
|
||||
match = team.matches.create!(opponent_name: "Setup", sets_to_win: 3)
|
||||
StreamSession.create!(match: match, user: user, platform: "matchlivetv", status: "idle")
|
||||
|
||||
expect(match.coach_hub_visible?).to be(true)
|
||||
end
|
||||
|
||||
it "mostra partita programmata futura mai trasmessa" do
|
||||
match = team.matches.create!(
|
||||
opponent_name: "Future",
|
||||
scheduled_at: 2.hours.from_now,
|
||||
sets_to_win: 3
|
||||
)
|
||||
|
||||
expect(match.coach_hub_visible?).to be(true)
|
||||
end
|
||||
|
||||
it "nasconde bozza abbandonata senza sessione" do
|
||||
match = team.matches.create!(opponent_name: "Avversario", sets_to_win: 3)
|
||||
|
||||
expect(match.coach_hub_visible?).to be(false)
|
||||
end
|
||||
|
||||
it "nasconde partita già trasmessa e terminata" do
|
||||
match = team.matches.create!(opponent_name: "Done", sets_to_win: 3)
|
||||
StreamSession.create!(match: match, user: user, platform: "matchlivetv", status: "ended")
|
||||
|
||||
expect(match.coach_hub_visible?).to be(false)
|
||||
end
|
||||
|
||||
it "nasconde partita programmata nel passato mai trasmessa" do
|
||||
match = team.matches.create!(
|
||||
opponent_name: "Missed",
|
||||
scheduled_at: 2.hours.ago,
|
||||
sets_to_win: 3
|
||||
)
|
||||
|
||||
expect(match.coach_hub_visible?).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
describe "scoring_rules" do
|
||||
it "accetta regole personalizzate valide" do
|
||||
match = team.matches.new(
|
||||
opponent_name: "Opp",
|
||||
sets_to_win: 2,
|
||||
scoring_rules: { points_per_set: 21, points_deciding_set: 15, min_point_lead: 2 }
|
||||
)
|
||||
|
||||
expect(match).to be_valid
|
||||
end
|
||||
|
||||
it "rifiuta punteggi non positivi" do
|
||||
match = team.matches.new(
|
||||
opponent_name: "Opp",
|
||||
scoring_rules: { points_per_set: 0 }
|
||||
)
|
||||
|
||||
expect(match).not_to be_valid
|
||||
expect(match.errors[:scoring_rules]).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user