Admin analytics: heatmap per device, anteprima staff e analisi costi.

Separa heatmap mobile/desktop, opt-out analytics per operatori, dashboard costi con KPI e trend mensili.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-28 19:25:38 +02:00
co-authored by Cursor
parent e0e07e5316
commit 29c8afb94d
43 changed files with 1572 additions and 58 deletions
+132
View File
@@ -0,0 +1,132 @@
# frozen_string_literal: true
# Dati simulati per la pagina Admin → Costi (trend 12 mesi, società, revenue).
# Uso: bundle exec rails runner db/seeds/cost_analytics_demo.rb
load Rails.root.join("db/seeds/plans.rb")
coach = User.find_or_create_by!(email: "coach@matchlivetv.test") do |u|
u.name = "Coach Demo"
u.password = "Password123"
u.role = "coach"
end
clubs_data = [
{ name: "Tigers Volley", sport: "volleyball", plan: "premium_full", sessions: 14, hours: 18.5, revenue_yearly: 19900 },
{ name: "ASD Eagles Milano", sport: "volleyball", plan: "premium_light", sessions: 9, hours: 11.0, revenue_yearly: 5900 },
{ name: "Volley Stars Roma", sport: "volleyball", plan: "premium_full", sessions: 6, hours: 8.0, revenue_yearly: 19900 },
{ name: "Basket Juventus U18", sport: "basketball", plan: "premium_light", sessions: 4, hours: 5.5, revenue_yearly: 5900 },
{ name: "Padova Beach", sport: "volleyball", plan: "free", sessions: 2, hours: 2.0, revenue_yearly: 0 }
]
sport_keys = {
"volleyball" => "pallavolo",
"basketball" => "basket"
}
clubs = clubs_data.map do |row|
club = Club.find_or_create_by!(name: row[:name]) { |c| c.sport = row[:sport] }
Billing::AssignPlan.call(club: club, plan_slug: row[:plan])
ClubMembership.find_or_create_by!(user: coach, club: club) { |m| m.role = "owner" }
team = club.teams.find_or_create_by!(name: "Prima squadra") do |t|
t.sport_key = sport_keys.fetch(row[:sport], "pallavolo")
end
{ club: club, team: team, **row }
end
month_costs = [
{ offset: 11, hetzner: 89.00, stream: 12.50, s3: 8.20 },
{ offset: 10, hetzner: 89.00, stream: 18.00, s3: 9.10 },
{ offset: 9, hetzner: 89.00, stream: 24.50, s3: 10.40 },
{ offset: 8, hetzner: 89.00, stream: 31.00, s3: 11.80 },
{ offset: 7, hetzner: 89.00, stream: 22.00, s3: 12.20 },
{ offset: 6, hetzner: 89.00, stream: 15.50, s3: 13.50 },
{ offset: 5, hetzner: 89.00, stream: 28.00, s3: 14.80 },
{ offset: 4, hetzner: 89.00, stream: 35.50, s3: 15.60 },
{ offset: 3, hetzner: 89.00, stream: 42.00, s3: 16.90 },
{ offset: 2, hetzner: 89.00, stream: 38.00, s3: 18.20 },
{ offset: 1, hetzner: 89.00, stream: 45.00, s3: 19.50 },
{ offset: 0, hetzner: 89.00, stream: 52.00, s3: 21.00 }
]
PlatformCostEntry.delete_all
month_costs.each do |row|
month = Time.zone.today.beginning_of_month - row[:offset].months
[
{ label: "Hetzner home + backup", amount: row[:hetzner] },
{ label: "Nodi stream CPX (overflow)", amount: row[:stream] },
{ label: "Garage/S3 replay storage", amount: row[:s3] }
].each do |entry|
PlatformCostEntry.create!(
month: month,
label: entry[:label],
amount_cents: (entry[:amount] * 100).round,
notes: "Dato demo seed"
)
end
end
# Sessioni e pagamenti nel mese corrente (distribuiti tra società)
current_month = Time.zone.today.beginning_of_month
clubs.each_with_index do |row, index|
club = row[:club]
team = row[:team]
sessions_count = row[:sessions]
total_secs = (row[:hours] * 3600).round
sessions_count.times do |n|
secs = (total_secs / sessions_count.to_f).round
day = current_month + (n % 27).days
started = day.change(hour: 10 + (n % 6), min: 0)
ended = started + secs.seconds
match = team.matches.create!(
opponent_name: "Avversario demo #{n + 1}",
sport_key: team.sport_key,
scheduled_at: started
)
StreamSession.create!(
match: match,
user: coach,
platform: "matchlivetv",
status: "ended",
started_at: started,
ended_at: ended,
total_duration_secs: secs
)
end
if row[:revenue_yearly].positive?
payment = Billing::Payment.find_or_initialize_by(club: club, paid_at: current_month + (index + 3).days)
payment.assign_attributes(
status: "paid",
provider: "stripe",
amount_cents: row[:revenue_yearly],
currency: "EUR",
plan_slug: row[:plan]
)
payment.save!
end
# Storage simulato (cumulativo)
sample_session = team.matches.last&.stream_sessions&.first
next unless sample_session
rec = Recording.find_or_initialize_by(stream_session: sample_session)
rec.assign_attributes(
team: team,
status: "ready",
privacy_status: "unlisted",
storage_backend: "s3",
storage_policy: "retained",
byte_size: (500_000_000 + index * 180_000_000),
duration_secs: 3600
)
rec.save!
end
puts "Cost analytics demo OK:"
puts " Voci costo: #{PlatformCostEntry.count} (12 mesi)"
puts " Sessioni demo mese corrente: #{StreamSession.where(status: 'ended', ended_at: current_month..current_month.end_of_month).count}"
puts " Admin: http://localhost:3000/admin/costs"
puts " Login admin: admin / AdminPass123"