Limita una diretta attiva per account e valorizza la copertina Premium Full.
Blocca la seconda diretta sullo stesso utente, registra gli abusi in admin e presenta la copertina come grafica caricata dalla società, con demo Team MLTV. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,6 +12,7 @@ module Admin
|
|||||||
@plans = Plan.ordered.reject { |p| p.slug == "free" }
|
@plans = Plan.ordered.reject { |p| p.slug == "free" }
|
||||||
@teams = @club.teams.order(:name)
|
@teams = @club.teams.order(:name)
|
||||||
@quote = @club.active_billing_quote
|
@quote = @club.active_billing_quote
|
||||||
|
@concurrency_violations = StreamConcurrencyViolation.for_club(@club.id).recent.limit(20)
|
||||||
end
|
end
|
||||||
|
|
||||||
def grant_comped
|
def grant_comped
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ module Admin
|
|||||||
.includes(:stream_node, match: :team)
|
.includes(:stream_node, match: :team)
|
||||||
.order(started_at: :desc)
|
.order(started_at: :desc)
|
||||||
@teams = Team.includes(:matches).order(:name).limit(8)
|
@teams = Team.includes(:matches).order(:name).limit(8)
|
||||||
|
@recent_concurrency_violations = StreamConcurrencyViolation.recent.limit(8)
|
||||||
|
@concurrency_violation_lookback = StreamConcurrencyViolation.lookback.count
|
||||||
end
|
end
|
||||||
|
|
||||||
def metrics
|
def metrics
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ module Admin
|
|||||||
.find(params[:id])
|
.find(params[:id])
|
||||||
@events = @session.stream_events.recent.limit(100)
|
@events = @session.stream_events.recent.limit(100)
|
||||||
@club = @session.match.team.club
|
@club = @session.match.team.club
|
||||||
|
@concurrency_violations = StreamConcurrencyViolation.for_session(@session.id).recent.limit(20)
|
||||||
end
|
end
|
||||||
|
|
||||||
def stop
|
def stop
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Admin
|
||||||
|
class StreamConcurrencyViolationsController < Admin::BaseController
|
||||||
|
def index
|
||||||
|
@filters = {
|
||||||
|
q: params[:q].to_s.strip.presence,
|
||||||
|
devices_differ: params[:devices_differ].to_s == "1"
|
||||||
|
}
|
||||||
|
scope = StreamConcurrencyViolation.recent
|
||||||
|
if @filters[:q]
|
||||||
|
term = "%#{ActiveRecord::Base.sanitize_sql_like(@filters[:q])}%"
|
||||||
|
scope = scope.where(
|
||||||
|
"user_email ILIKE :term OR user_name ILIKE :term OR occupying_club_name ILIKE :term OR attempted_club_name ILIKE :term OR occupying_match_label ILIKE :term OR attempted_match_label ILIKE :term",
|
||||||
|
term: term
|
||||||
|
)
|
||||||
|
end
|
||||||
|
scope = scope.two_devices if @filters[:devices_differ]
|
||||||
|
@total_count = scope.count
|
||||||
|
@violations = scope.limit(200)
|
||||||
|
@lookback_count = StreamConcurrencyViolation.lookback.count
|
||||||
|
@two_devices_count = StreamConcurrencyViolation.lookback.two_devices.count
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -188,4 +188,8 @@ module AdminHelper
|
|||||||
labels = item.selected_channels.map { |key| I18n.t("admin.announcements.channels.#{key}") }
|
labels = item.selected_channels.map { |key| I18n.t("admin.announcements.channels.#{key}") }
|
||||||
labels.presence&.join(" · ") || I18n.t("admin.common.dash")
|
labels.presence&.join(" · ") || I18n.t("admin.common.dash")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def admin_concurrency_violation_lookback_count
|
||||||
|
@admin_concurrency_violation_lookback_count ||= StreamConcurrencyViolation.lookback.count
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class StreamConcurrencyViolation < ApplicationRecord
|
||||||
|
LOOKBACK = 30.days
|
||||||
|
|
||||||
|
belongs_to :user, optional: true
|
||||||
|
belongs_to :occupying_session, class_name: "StreamSession", optional: true
|
||||||
|
belongs_to :attempted_session, class_name: "StreamSession", optional: true
|
||||||
|
belongs_to :occupying_club, class_name: "Club", optional: true
|
||||||
|
belongs_to :attempted_club, class_name: "Club", optional: true
|
||||||
|
|
||||||
|
validates :user_email, :occupying_match_label, :attempted_match_label, presence: true
|
||||||
|
validates :attempt_action, inclusion: { in: %w[start resume] }
|
||||||
|
|
||||||
|
scope :recent, -> { order(created_at: :desc) }
|
||||||
|
scope :since, ->(time) { where("created_at >= ?", time) }
|
||||||
|
scope :lookback, -> { since(LOOKBACK.ago) }
|
||||||
|
scope :two_devices, -> { where(devices_differ: true) }
|
||||||
|
scope :for_club, lambda { |club_id|
|
||||||
|
where("occupying_club_id = :id OR attempted_club_id = :id", id: club_id)
|
||||||
|
}
|
||||||
|
scope :for_session, lambda { |session_id|
|
||||||
|
where("occupying_session_id = :id OR attempted_session_id = :id", id: session_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
def self.record!(attempted:, occupying:, action: "start")
|
||||||
|
user = attempted.user || occupying.user
|
||||||
|
occupying_club = occupying.match&.team&.club
|
||||||
|
attempted_club = attempted.match&.team&.club
|
||||||
|
occupying_device = occupying.client_device_label
|
||||||
|
attempted_device = attempted.client_device_label
|
||||||
|
|
||||||
|
create!(
|
||||||
|
user: user,
|
||||||
|
occupying_session: occupying,
|
||||||
|
attempted_session: attempted,
|
||||||
|
occupying_club: occupying_club,
|
||||||
|
attempted_club: attempted_club,
|
||||||
|
attempt_action: action.to_s,
|
||||||
|
user_email: user&.email.presence || "unknown",
|
||||||
|
user_name: user&.name,
|
||||||
|
occupying_club_name: occupying_club&.name,
|
||||||
|
attempted_club_name: attempted_club&.name,
|
||||||
|
occupying_match_label: occupying.match_label,
|
||||||
|
attempted_match_label: attempted.match_label,
|
||||||
|
occupying_status: occupying.status,
|
||||||
|
occupying_device: occupying_device,
|
||||||
|
attempted_device: attempted_device,
|
||||||
|
devices_differ: StreamSession.devices_differ?(occupying, attempted),
|
||||||
|
metadata: {
|
||||||
|
occupying_session_id: occupying.id,
|
||||||
|
attempted_session_id: attempted.id,
|
||||||
|
occupying_client_os: occupying.client_os,
|
||||||
|
attempted_client_os: attempted.client_os,
|
||||||
|
occupying_app_version: occupying.app_version,
|
||||||
|
attempted_app_version: attempted.app_version
|
||||||
|
}.compact
|
||||||
|
)
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.error("[StreamConcurrencyViolation] record failed: #{e.class} #{e.message}")
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def club_name
|
||||||
|
attempted_club_name.presence || occupying_club_name
|
||||||
|
end
|
||||||
|
|
||||||
|
def operator_label
|
||||||
|
[user_name.presence, user_email].compact.join(" · ")
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -10,6 +10,10 @@ class StreamSession < ApplicationRecord
|
|||||||
belongs_to :user
|
belongs_to :user
|
||||||
belongs_to :stream_node, optional: true
|
belongs_to :stream_node, optional: true
|
||||||
has_many :stream_events, dependent: :destroy
|
has_many :stream_events, dependent: :destroy
|
||||||
|
has_many :occupying_concurrency_violations, class_name: "StreamConcurrencyViolation",
|
||||||
|
foreign_key: :occupying_session_id, dependent: :nullify, inverse_of: :occupying_session
|
||||||
|
has_many :attempted_concurrency_violations, class_name: "StreamConcurrencyViolation",
|
||||||
|
foreign_key: :attempted_session_id, dependent: :nullify, inverse_of: :attempted_session
|
||||||
has_one :score_state, dependent: :destroy
|
has_one :score_state, dependent: :destroy
|
||||||
has_one :recording
|
has_one :recording
|
||||||
has_many :device_states, dependent: :destroy
|
has_many :device_states, dependent: :destroy
|
||||||
@@ -84,6 +88,35 @@ class StreamSession < ApplicationRecord
|
|||||||
stream_node&.role.presence || ingest_role
|
stream_node&.role.presence || ingest_role
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def match_label
|
||||||
|
team_name = match&.team&.name
|
||||||
|
opponent = match&.opponent_name
|
||||||
|
return id.to_s if team_name.blank?
|
||||||
|
|
||||||
|
opponent.present? ? "#{team_name} vs #{opponent}" : team_name
|
||||||
|
end
|
||||||
|
|
||||||
|
def client_device_label
|
||||||
|
parts = []
|
||||||
|
parts << client_os if client_os.present?
|
||||||
|
device = [device_manufacturer, device_model].compact_blank.join(" ")
|
||||||
|
parts << device if device.present?
|
||||||
|
parts << "OS #{os_version}" if os_version.present?
|
||||||
|
parts.join(" · ").presence
|
||||||
|
end
|
||||||
|
|
||||||
|
def client_device_key
|
||||||
|
[client_os, device_manufacturer, device_model].map { |v| v.to_s.strip.downcase }.join("|")
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.devices_differ?(left, right)
|
||||||
|
ka = left.client_device_key
|
||||||
|
kb = right.client_device_key
|
||||||
|
return false if ka.delete("|").blank? || kb.delete("|").blank?
|
||||||
|
|
||||||
|
ka != kb
|
||||||
|
end
|
||||||
|
|
||||||
def rtmp_ingest_url
|
def rtmp_ingest_url
|
||||||
# RootEncoder richiede rtmp://host:port/app/stream (due segmenti).
|
# RootEncoder richiede rtmp://host:port/app/stream (due segmenti).
|
||||||
# MediaMTX path = live/match_{uuid} (no ?token= nel path).
|
# MediaMTX path = live/match_{uuid} (no ?token= nel path).
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class User < ApplicationRecord
|
|||||||
has_many :clubs, through: :club_memberships
|
has_many :clubs, through: :club_memberships
|
||||||
has_many :owned_clubs, -> { where(club_memberships: { role: "owner" }) }, through: :club_memberships, source: :club
|
has_many :owned_clubs, -> { where(club_memberships: { role: "owner" }) }, through: :club_memberships, source: :club
|
||||||
has_many :stream_sessions, dependent: :nullify
|
has_many :stream_sessions, dependent: :nullify
|
||||||
|
has_many :stream_concurrency_violations, dependent: :nullify
|
||||||
|
|
||||||
def manageable_teams
|
def manageable_teams
|
||||||
staff_ids = teams.select(:id)
|
staff_ids = teams.select(:id)
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Sessions
|
||||||
|
class AssertUserConcurrent
|
||||||
|
ERROR_CODE = "user_concurrent_stream"
|
||||||
|
|
||||||
|
def self.with_lock(session, action: "start")
|
||||||
|
new(session, action: action).with_lock { yield }
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(session, action: "start")
|
||||||
|
@session = session
|
||||||
|
@action = action.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
def with_lock
|
||||||
|
occupying = nil
|
||||||
|
|
||||||
|
User.transaction do
|
||||||
|
User.lock.find(@session.user_id) if @session.user_id.present?
|
||||||
|
occupying = occupying_session
|
||||||
|
yield if occupying.nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
return if occupying.nil?
|
||||||
|
|
||||||
|
StreamConcurrencyViolation.record!(
|
||||||
|
attempted: @session,
|
||||||
|
occupying: occupying,
|
||||||
|
action: @action
|
||||||
|
)
|
||||||
|
raise Teams::EntitlementError.new(
|
||||||
|
I18n.t("api.errors.user_concurrent_stream"),
|
||||||
|
code: ERROR_CODE
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def occupying_session
|
||||||
|
return if @session.user_id.blank?
|
||||||
|
|
||||||
|
StreamSession.broadcasting
|
||||||
|
.where(user_id: @session.user_id)
|
||||||
|
.where.not(id: @session.id)
|
||||||
|
.includes(:user, match: { team: :club })
|
||||||
|
.order(Arel.sql("COALESCE(started_at, updated_at) DESC"))
|
||||||
|
.first
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -10,8 +10,10 @@ module Sessions
|
|||||||
end
|
end
|
||||||
|
|
||||||
cancel_timeout_job
|
cancel_timeout_job
|
||||||
# connecting finché RTMP non è online (evita lose_connection da PublisherSync)
|
Sessions::AssertUserConcurrent.with_lock(@session, action: "resume") do
|
||||||
@session.begin_connect! if @session.may_begin_connect?
|
# connecting finché RTMP non è online (evita lose_connection da PublisherSync)
|
||||||
|
@session.begin_connect! if @session.may_begin_connect?
|
||||||
|
end
|
||||||
# Recording riabilitato in PublisherSync quando RTMP è online (evita patch path prima del publisher).
|
# Recording riabilitato in PublisherSync quando RTMP è online (evita patch path prima del publisher).
|
||||||
log_event("resumed")
|
log_event("resumed")
|
||||||
SessionChannel.broadcast_message(@session, { type: "command", action: "resume_stream" })
|
SessionChannel.broadcast_message(@session, { type: "command", action: "resume_stream" })
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ module Sessions
|
|||||||
end
|
end
|
||||||
|
|
||||||
def call
|
def call
|
||||||
@session.match.team.entitlements.assert_concurrent_stream!(excluding_session: @session)
|
Sessions::AssertUserConcurrent.with_lock(@session, action: "start") do
|
||||||
@session.begin_connect! if @session.may_begin_connect?
|
@session.match.team.entitlements.assert_concurrent_stream!(excluding_session: @session)
|
||||||
@session.update!(status: "connecting") unless @session.connecting?
|
@session.begin_connect! if @session.may_begin_connect?
|
||||||
|
@session.update!(status: "connecting") unless @session.connecting?
|
||||||
|
end
|
||||||
Youtube::LivePipeline.schedule!(@session, force: true) if @session.platform == "youtube"
|
Youtube::LivePipeline.schedule!(@session, force: true) if @session.platform == "youtube"
|
||||||
broadcast_status("connecting")
|
broadcast_status("connecting")
|
||||||
@session
|
@session
|
||||||
|
|||||||
@@ -46,3 +46,14 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<h3 style="font-size:1rem;margin-top:28px"><%= t("admin.clubs.show.concurrency_title") %></h3>
|
||||||
|
<p class="muted"><%= t("admin.clubs.show.concurrency_lead") %></p>
|
||||||
|
<%= render "admin/stream_concurrency_violations/table",
|
||||||
|
violations: @concurrency_violations,
|
||||||
|
empty_key: "admin.clubs.show.concurrency_none" %>
|
||||||
|
<% if @concurrency_violations.any? %>
|
||||||
|
<p class="kpi-sub" style="margin-top:0.75rem">
|
||||||
|
<%= link_to t("admin.clubs.show.concurrency_all"), admin_stream_concurrency_violations_path(q: @club.name) %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
|||||||
@@ -67,6 +67,24 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="panel" style="margin-bottom:1.25rem">
|
||||||
|
<h2><%= t("admin.dashboard.concurrency_panel.title") %></h2>
|
||||||
|
<% if @concurrency_violation_lookback.positive? %>
|
||||||
|
<p class="kpi-sub" style="margin-bottom:0.75rem">
|
||||||
|
<%= t("admin.dashboard.concurrency_panel.count", count: @concurrency_violation_lookback) %>
|
||||||
|
— <%= link_to t("admin.dashboard.concurrency_panel.view_all"), admin_stream_concurrency_violations_path %>
|
||||||
|
</p>
|
||||||
|
<%= render "admin/stream_concurrency_violations/table",
|
||||||
|
violations: @recent_concurrency_violations,
|
||||||
|
empty_key: "admin.dashboard.concurrency_panel.none" %>
|
||||||
|
<% else %>
|
||||||
|
<p class="empty" style="margin:0">
|
||||||
|
<%= t("admin.dashboard.concurrency_panel.none_html",
|
||||||
|
link: link_to(t("admin.dashboard.concurrency_panel.view_all"), admin_stream_concurrency_violations_path)) %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="charts-grid" style="grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));">
|
<section class="charts-grid" style="grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));">
|
||||||
<div class="chart-card">
|
<div class="chart-card">
|
||||||
<h3><%= t("admin.dashboard.disk.system_title") %></h3>
|
<h3><%= t("admin.dashboard.disk.system_title") %></h3>
|
||||||
|
|||||||
@@ -258,6 +258,16 @@
|
|||||||
</section>
|
</section>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<% if @concurrency_violations.any? %>
|
||||||
|
<section class="panel">
|
||||||
|
<h3><%= t("admin.sessions.show.concurrency_title") %></h3>
|
||||||
|
<p class="muted"><%= t("admin.sessions.show.concurrency_lead") %></p>
|
||||||
|
<%= render "admin/stream_concurrency_violations/table",
|
||||||
|
violations: @concurrency_violations,
|
||||||
|
empty_key: "admin.sessions.show.concurrency_none" %>
|
||||||
|
</section>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<h3><%= t("admin.sessions.show.events_title") %></h3>
|
<h3><%= t("admin.sessions.show.events_title") %></h3>
|
||||||
<% if @events.any? %>
|
<% if @events.any? %>
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<% if violations.any? %>
|
||||||
|
<div class="admin-table-wrap">
|
||||||
|
<table class="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><%= t("admin.stream_concurrency.table.when") %></th>
|
||||||
|
<th><%= t("admin.stream_concurrency.table.account") %></th>
|
||||||
|
<th><%= t("admin.stream_concurrency.table.club") %></th>
|
||||||
|
<th><%= t("admin.stream_concurrency.table.occupying") %></th>
|
||||||
|
<th><%= t("admin.stream_concurrency.table.attempted") %></th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% violations.each do |row| %>
|
||||||
|
<tr class="<%= 'admin-row--two-devices' if row.devices_differ? %>">
|
||||||
|
<td class="muted"><%= admin_datetime(row.created_at, with_seconds: true) %></td>
|
||||||
|
<td>
|
||||||
|
<strong><%= row.user_name.presence || t("admin.common.dash") %></strong>
|
||||||
|
<div class="muted" style="font-size:0.85rem"><%= row.user_email %></div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<% if row.attempted_club %>
|
||||||
|
<%= link_to row.club_name, admin_club_path(row.attempted_club) %>
|
||||||
|
<% elsif row.occupying_club %>
|
||||||
|
<%= link_to row.club_name, admin_club_path(row.occupying_club) %>
|
||||||
|
<% else %>
|
||||||
|
<%= row.club_name.presence || t("admin.common.dash") %>
|
||||||
|
<% end %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<% if row.occupying_session %>
|
||||||
|
<%= link_to row.occupying_match_label, admin_session_path(row.occupying_session) %>
|
||||||
|
<% else %>
|
||||||
|
<%= row.occupying_match_label %>
|
||||||
|
<% end %>
|
||||||
|
<div class="muted" style="font-size:0.85rem;margin-top:0.2rem">
|
||||||
|
<span class="badge <%= admin_session_status_badge_class(row.occupying_status) %>"><%= row.occupying_status %></span>
|
||||||
|
<%= row.occupying_device.presence || t("admin.common.dash") %>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<% if row.attempted_session %>
|
||||||
|
<%= link_to row.attempted_match_label, admin_session_path(row.attempted_session) %>
|
||||||
|
<% else %>
|
||||||
|
<%= row.attempted_match_label %>
|
||||||
|
<% end %>
|
||||||
|
<div class="muted" style="font-size:0.85rem;margin-top:0.2rem">
|
||||||
|
<%= t("admin.stream_concurrency.action.#{row.attempt_action}") %>
|
||||||
|
· <%= row.attempted_device.presence || t("admin.common.dash") %>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<% if row.devices_differ? %>
|
||||||
|
<span class="badge badge--abuse"><%= t("admin.stream_concurrency.badge.two_devices") %></span>
|
||||||
|
<% else %>
|
||||||
|
<span class="muted"><%= t("admin.stream_concurrency.badge.same_or_unknown") %></span>
|
||||||
|
<% end %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<p class="empty"><%= t(empty_key) %></p>
|
||||||
|
<% end %>
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<% content_for :body_class, "admin-body" %>
|
||||||
|
|
||||||
|
<div class="admin-page-head">
|
||||||
|
<h2 class="admin-page-title"><%= t("admin.stream_concurrency.index.title") %></h2>
|
||||||
|
<p class="muted admin-page-sub"><%= t("admin.stream_concurrency.index.lead") %></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="kpi-grid">
|
||||||
|
<div class="kpi-card <%= @lookback_count.positive? ? 'kpi-card--accent' : '' %>">
|
||||||
|
<div class="kpi-label"><%= t("admin.stream_concurrency.kpi.lookback") %></div>
|
||||||
|
<div class="kpi-value"><%= @lookback_count %></div>
|
||||||
|
<div class="kpi-sub"><%= t("admin.stream_concurrency.kpi.lookback_sub") %></div>
|
||||||
|
</div>
|
||||||
|
<div class="kpi-card <%= @two_devices_count.positive? ? 'kpi-card--danger' : '' %>">
|
||||||
|
<div class="kpi-label"><%= t("admin.stream_concurrency.kpi.two_devices") %></div>
|
||||||
|
<div class="kpi-value"><%= @two_devices_count %></div>
|
||||||
|
<div class="kpi-sub"><%= t("admin.stream_concurrency.kpi.two_devices_sub") %></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="panel admin-sessions-filters">
|
||||||
|
<%= form_with url: admin_stream_concurrency_violations_path, method: :get, local: true, class: "admin-filter-form" do %>
|
||||||
|
<div class="admin-filter-grid">
|
||||||
|
<label class="admin-filter-field">
|
||||||
|
<span><%= t("admin.stream_concurrency.filters.q") %></span>
|
||||||
|
<%= text_field_tag :q, @filters[:q], placeholder: t("admin.stream_concurrency.filters.q_placeholder") %>
|
||||||
|
</label>
|
||||||
|
<label class="admin-filter-field admin-filter-field--check">
|
||||||
|
<span><%= t("admin.stream_concurrency.filters.two_devices") %></span>
|
||||||
|
<label class="admin-checkbox">
|
||||||
|
<%= check_box_tag :devices_differ, "1", @filters[:devices_differ] %>
|
||||||
|
<%= t("admin.stream_concurrency.filters.two_devices_hint") %>
|
||||||
|
</label>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="admin-filter-actions">
|
||||||
|
<%= submit_tag t("admin.stream_concurrency.filters.apply"), class: "admin-btn admin-btn--primary admin-btn--sm" %>
|
||||||
|
<%= link_to t("admin.stream_concurrency.filters.reset"), admin_stream_concurrency_violations_path, class: "admin-btn admin-btn--outline admin-btn--sm" %>
|
||||||
|
<span class="muted admin-filter-count">
|
||||||
|
<%= t("admin.stream_concurrency.index.results", shown: @violations.size, total: @total_count) %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<%= render "admin/stream_concurrency_violations/table", violations: @violations, empty_key: "admin.stream_concurrency.index.none" %>
|
||||||
|
</div>
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="robots" content="noindex, nofollow">
|
<meta name="robots" content="noindex, nofollow">
|
||||||
<%= csrf_meta_tags %>
|
<%= csrf_meta_tags %>
|
||||||
<link rel="stylesheet" href="/admin.css?v=15">
|
<link rel="stylesheet" href="/admin.css?v=16">
|
||||||
<%= yield :head %>
|
<%= yield :head %>
|
||||||
<% if content_for?(:replay_archive_styles) %>
|
<% if content_for?(:replay_archive_styles) %>
|
||||||
<link rel="stylesheet" href="/marketing.css?v=42">
|
<link rel="stylesheet" href="/marketing.css?v=42">
|
||||||
@@ -32,6 +32,10 @@
|
|||||||
<%= link_to t("admin.layout.nav.billing"), admin_billing_path, class: ("active" if controller_name.in?(%w[billing billing_invoices])) %>
|
<%= link_to t("admin.layout.nav.billing"), admin_billing_path, class: ("active" if controller_name.in?(%w[billing billing_invoices])) %>
|
||||||
<%= link_to t("admin.layout.nav.youtube"), admin_youtube_platform_path, class: ("active" if controller_name == "youtube") %>
|
<%= link_to t("admin.layout.nav.youtube"), admin_youtube_platform_path, class: ("active" if controller_name == "youtube") %>
|
||||||
<%= link_to t("admin.layout.nav.sessions"), admin_sessions_path, class: ("active" if controller_name == "sessions") %>
|
<%= link_to t("admin.layout.nav.sessions"), admin_sessions_path, class: ("active" if controller_name == "sessions") %>
|
||||||
|
<% abuse_count = admin_concurrency_violation_lookback_count %>
|
||||||
|
<%= link_to admin_stream_concurrency_violations_path, class: ("active" if controller_name == "stream_concurrency_violations") do %>
|
||||||
|
<%= t("admin.layout.nav.stream_concurrency") %><% if abuse_count.positive? %> <span class="admin-nav-badge"><%= abuse_count %></span><% end %>
|
||||||
|
<% end %>
|
||||||
<%= link_to t("admin.layout.nav.analytics"), admin_analytics_path, class: ("active" if controller_name == "analytics") %>
|
<%= link_to t("admin.layout.nav.analytics"), admin_analytics_path, class: ("active" if controller_name == "analytics") %>
|
||||||
<%= link_to t("admin.layout.nav.costs"), admin_costs_path, class: ("active" if controller_name.in?(%w[costs cost_entries])) %>
|
<%= link_to t("admin.layout.nav.costs"), admin_costs_path, class: ("active" if controller_name.in?(%w[costs cost_entries])) %>
|
||||||
<%= link_to t("admin.layout.nav.stream_nodes"), admin_stream_nodes_path, class: ("active" if controller_name == "stream_nodes") %>
|
<%= link_to t("admin.layout.nav.stream_nodes"), admin_stream_nodes_path, class: ("active" if controller_name == "stream_nodes") %>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<%= render "shared/analytics_suppress" %>
|
<%= render "shared/analytics_suppress" %>
|
||||||
<%= yield :head %>
|
<%= yield :head %>
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer">
|
||||||
<link rel="stylesheet" href="/marketing.css?v=79">
|
<link rel="stylesheet" href="/marketing.css?v=81">
|
||||||
</head>
|
</head>
|
||||||
<body data-confirm-i18n='<%= raw confirm_dialog_i18n_json %>'<% if MatchLiveTv.google_analytics_configured? %> data-ga-id="<%= MatchLiveTv.google_analytics_measurement_id %>"<% end %>>
|
<body data-confirm-i18n='<%= raw confirm_dialog_i18n_json %>'<% if MatchLiveTv.google_analytics_configured? %> data-ga-id="<%= MatchLiveTv.google_analytics_measurement_id %>"<% end %>>
|
||||||
<%= render "shared/cookie_banner" %>
|
<%= render "shared/cookie_banner" %>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<%= render "shared/meta_tags" %>
|
<%= render "shared/meta_tags" %>
|
||||||
<%= render "shared/analytics_suppress" %>
|
<%= render "shared/analytics_suppress" %>
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer">
|
||||||
<link rel="stylesheet" href="/marketing.css?v=79">
|
<link rel="stylesheet" href="/marketing.css?v=81">
|
||||||
<link rel="stylesheet" href="/live.css?v=26">
|
<link rel="stylesheet" href="/live.css?v=26">
|
||||||
<%= yield :head %>
|
<%= yield :head %>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -164,8 +164,8 @@
|
|||||||
<aside class="demo-live-card" aria-label="<%= t("live.index.demo_aria_label") %>">
|
<aside class="demo-live-card" aria-label="<%= t("live.index.demo_aria_label") %>">
|
||||||
<p class="demo-label"><%= t("live.index.demo_label") %></p>
|
<p class="demo-label"><%= t("live.index.demo_label") %></p>
|
||||||
<article class="live-card live-card--demo">
|
<article class="live-card live-card--demo">
|
||||||
<h3>Tigers Volley vs ASD Eagles</h3>
|
<h3><%= MatchLiveTv::Demo.match_title %></h3>
|
||||||
<p class="meta"><%= t("live.index.demo_meta") %></p>
|
<p class="meta"><%= MatchLiveTv::Demo.live_meta %></p>
|
||||||
<p class="card-score">
|
<p class="card-score">
|
||||||
<span class="card-sets"><%= t("live.index.demo_sets") %></span>
|
<span class="card-sets"><%= t("live.index.demo_sets") %></span>
|
||||||
<span class="card-points">18 - 16</span>
|
<span class="card-points">18 - 16</span>
|
||||||
|
|||||||
@@ -80,6 +80,13 @@
|
|||||||
) %>
|
) %>
|
||||||
</p>
|
</p>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<details class="faq-item">
|
||||||
|
<summary><%= t("pages.faq.q10_question") %></summary>
|
||||||
|
<p>
|
||||||
|
<%= t("pages.faq.q10_answer") %>
|
||||||
|
</p>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
|
|||||||
@@ -187,7 +187,7 @@
|
|||||||
<div class="features-yt-mock__meta">
|
<div class="features-yt-mock__meta">
|
||||||
<span class="features-yt-mock__avatar"><i class="fa-solid fa-shield-halved"></i></span>
|
<span class="features-yt-mock__avatar"><i class="fa-solid fa-shield-halved"></i></span>
|
||||||
<div class="features-yt-mock__text">
|
<div class="features-yt-mock__text">
|
||||||
<strong><%= t("pages.features.youtube_mock_channel") %></strong>
|
<strong><%= MatchLiveTv::Demo.home_team %></strong>
|
||||||
<span><%= t("pages.features.youtube_mock_subs") %></span>
|
<span><%= t("pages.features.youtube_mock_subs") %></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -196,6 +196,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<%= render "shared/sponsor_cover_promo" %>
|
||||||
|
|
||||||
<section class="section wrap features-replay" aria-labelledby="features-replay-title">
|
<section class="section wrap features-replay" aria-labelledby="features-replay-title">
|
||||||
<div class="features-split features-split--reverse">
|
<div class="features-split features-split--reverse">
|
||||||
<div class="features-split__copy">
|
<div class="features-split__copy">
|
||||||
@@ -218,7 +220,7 @@
|
|||||||
<li>
|
<li>
|
||||||
<span class="features-archive-mock__thumb features-archive-mock__thumb--a"></span>
|
<span class="features-archive-mock__thumb features-archive-mock__thumb--a"></span>
|
||||||
<span class="features-archive-mock__info">
|
<span class="features-archive-mock__info">
|
||||||
<strong><%= t("pages.features.replay_mock_1_title") %></strong>
|
<strong><%= MatchLiveTv::Demo.match_title %></strong>
|
||||||
<em><%= t("pages.features.replay_mock_1_meta") %></em>
|
<em><%= t("pages.features.replay_mock_1_meta") %></em>
|
||||||
</span>
|
</span>
|
||||||
<i class="fa-solid fa-download" aria-hidden="true"></i>
|
<i class="fa-solid fa-download" aria-hidden="true"></i>
|
||||||
@@ -226,7 +228,7 @@
|
|||||||
<li>
|
<li>
|
||||||
<span class="features-archive-mock__thumb features-archive-mock__thumb--b"></span>
|
<span class="features-archive-mock__thumb features-archive-mock__thumb--b"></span>
|
||||||
<span class="features-archive-mock__info">
|
<span class="features-archive-mock__info">
|
||||||
<strong><%= t("pages.features.replay_mock_2_title") %></strong>
|
<strong><%= MatchLiveTv::Demo.match_title %></strong>
|
||||||
<em><%= t("pages.features.replay_mock_2_meta") %></em>
|
<em><%= t("pages.features.replay_mock_2_meta") %></em>
|
||||||
</span>
|
</span>
|
||||||
<i class="fa-solid fa-download" aria-hidden="true"></i>
|
<i class="fa-solid fa-download" aria-hidden="true"></i>
|
||||||
@@ -234,7 +236,7 @@
|
|||||||
<li>
|
<li>
|
||||||
<span class="features-archive-mock__thumb features-archive-mock__thumb--c"></span>
|
<span class="features-archive-mock__thumb features-archive-mock__thumb--c"></span>
|
||||||
<span class="features-archive-mock__info">
|
<span class="features-archive-mock__info">
|
||||||
<strong><%= t("pages.features.replay_mock_3_title") %></strong>
|
<strong><%= MatchLiveTv::Demo.match_title %></strong>
|
||||||
<em><%= t("pages.features.replay_mock_3_meta") %></em>
|
<em><%= t("pages.features.replay_mock_3_meta") %></em>
|
||||||
</span>
|
</span>
|
||||||
<i class="fa-solid fa-download" aria-hidden="true"></i>
|
<i class="fa-solid fa-download" aria-hidden="true"></i>
|
||||||
|
|||||||
@@ -94,6 +94,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<%= render "shared/sponsor_cover_promo", variant: :compact %>
|
||||||
|
|
||||||
<section class="section wrap plans-teaser">
|
<section class="section wrap plans-teaser">
|
||||||
<h2><%= t("home.plans_title") %></h2>
|
<h2><%= t("home.plans_title") %></h2>
|
||||||
<p class="plans-teaser-lead"><%= t("home.plans_lead") %></p>
|
<p class="plans-teaser-lead"><%= t("home.plans_lead") %></p>
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
|
|
||||||
<%= render "shared/plan_cards" %>
|
<%= render "shared/plan_cards" %>
|
||||||
|
|
||||||
|
<%= render "shared/sponsor_cover_promo", variant: :compact, show_cta: false, nested: true %>
|
||||||
|
|
||||||
<div class="table-scroll compare-table-wrap">
|
<div class="table-scroll compare-table-wrap">
|
||||||
<table class="compare-table">
|
<table class="compare-table">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
@@ -50,6 +52,12 @@
|
|||||||
<tr><td><%= t("pages.pricing.table_youtube") %></td><td><%= t("pages.pricing.table_no") %></td><td>Match Live TV</td><td><%= t("pages.pricing.table_youtube_club") %></td></tr>
|
<tr><td><%= t("pages.pricing.table_youtube") %></td><td><%= t("pages.pricing.table_no") %></td><td>Match Live TV</td><td><%= t("pages.pricing.table_youtube_club") %></td></tr>
|
||||||
<tr><td><%= t("pages.pricing.table_replay") %></td><td><%= t("pages.pricing.table_no") %></td><td><%= t("pages.plans.replay_days", count: 30) %></td><td><%= t("pages.plans.replay_days", count: 90) %></td></tr>
|
<tr><td><%= t("pages.pricing.table_replay") %></td><td><%= t("pages.pricing.table_no") %></td><td><%= t("pages.plans.replay_days", count: 30) %></td><td><%= t("pages.plans.replay_days", count: 90) %></td></tr>
|
||||||
<tr><td><%= t("pages.pricing.table_download") %></td><td><%= t("pages.pricing.table_no") %></td><td><%= t("pages.pricing.table_yes") %></td><td><%= t("pages.pricing.table_yes") %></td></tr>
|
<tr><td><%= t("pages.pricing.table_download") %></td><td><%= t("pages.pricing.table_no") %></td><td><%= t("pages.pricing.table_yes") %></td><td><%= t("pages.pricing.table_yes") %></td></tr>
|
||||||
|
<tr>
|
||||||
|
<td><%= t("pages.pricing.table_cover_sponsor") %></td>
|
||||||
|
<td><%= t("pages.pricing.table_no") %></td>
|
||||||
|
<td><%= t("pages.pricing.table_no") %></td>
|
||||||
|
<td><%= t("pages.pricing.table_yes") %></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= t("pages.pricing.table_price") %></td>
|
<td><%= t("pages.pricing.table_price") %></td>
|
||||||
<td><%= t("pages.pricing.table_price_free") %></td>
|
<td><%= t("pages.pricing.table_price_free") %></td>
|
||||||
|
|||||||
@@ -65,6 +65,9 @@
|
|||||||
t("pages.plans.youtube_none")
|
t("pages.plans.youtube_none")
|
||||||
end
|
end
|
||||||
) %></li>
|
) %></li>
|
||||||
|
<% if plan.slug == "premium_full" %>
|
||||||
|
<li><%= raw t("pages.plans.cover_sponsor_html") %></li>
|
||||||
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
<% if plan.slug == "premium_full" %>
|
<% if plan.slug == "premium_full" %>
|
||||||
<p class="plan-staff-note"><%= t("pages.plans.staff_note_full") %></p>
|
<p class="plan-staff-note"><%= t("pages.plans.staff_note_full") %></p>
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<figure class="cover-mock" aria-hidden="true" inert>
|
||||||
|
<div class="cover-mock__chrome">
|
||||||
|
<span class="cover-mock__brand">MATCH <em>LIVE</em> TV</span>
|
||||||
|
<span class="cover-mock__state"><%= t("pages.sponsor_cover.mock.state") %></span>
|
||||||
|
</div>
|
||||||
|
<div class="cover-mock__stage">
|
||||||
|
<p class="cover-mock__cat"><%= MatchLiveTv::Demo.category %></p>
|
||||||
|
<p class="cover-mock__home"><%= MatchLiveTv::Demo.home_team %></p>
|
||||||
|
<p class="cover-mock__vs">vs</p>
|
||||||
|
<p class="cover-mock__away"><%= MatchLiveTv::Demo.away_team %></p>
|
||||||
|
<p class="cover-mock__when"><%= MatchLiveTv::Demo.when_label %></p>
|
||||||
|
<p class="cover-mock__art-label"><%= t("pages.sponsor_cover.mock.art_label") %></p>
|
||||||
|
<div class="cover-mock__art">
|
||||||
|
<div class="cover-mock__crest">
|
||||||
|
<span class="cover-mock__crest-mark">M</span>
|
||||||
|
<span class="cover-mock__crest-name"><%= MatchLiveTv::Demo.home_team %></span>
|
||||||
|
</div>
|
||||||
|
<div class="cover-mock__marks">
|
||||||
|
<span class="cover-mock__mark cover-mock__mark--a"></span>
|
||||||
|
<span class="cover-mock__mark cover-mock__mark--b"></span>
|
||||||
|
<span class="cover-mock__mark cover-mock__mark--c"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="cover-mock__soon"><%= t("pages.sponsor_cover.mock.soon") %></p>
|
||||||
|
</div>
|
||||||
|
</figure>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<%# locals: (variant: :full, show_cta: nil, nested: false) %>
|
||||||
|
<% variant = (local_assigns[:variant] || :full).to_sym %>
|
||||||
|
<% compact = variant == :compact %>
|
||||||
|
<% nested = local_assigns[:nested] %>
|
||||||
|
<% show_cta = local_assigns.fetch(:show_cta, true) %>
|
||||||
|
<% title_id = compact ? "sponsor-cover-title-compact" : "sponsor-cover-title" %>
|
||||||
|
|
||||||
|
<section class="section<%= " wrap" unless nested %> sponsor-cover<%= " sponsor-cover--compact" if compact %>" aria-labelledby="<%= title_id %>">
|
||||||
|
<div class="sponsor-cover__panel">
|
||||||
|
<div class="sponsor-cover__copy">
|
||||||
|
<p class="feature-card__badge feature-card__badge--gold"><%= t("pages.sponsor_cover.eyebrow") %></p>
|
||||||
|
<h2 id="<%= title_id %>"><%= t("pages.sponsor_cover.title") %></h2>
|
||||||
|
<p class="sponsor-cover__lead"><%= t("pages.sponsor_cover.body") %></p>
|
||||||
|
<% unless compact %>
|
||||||
|
<ul class="features-checklist">
|
||||||
|
<li><%= t("pages.sponsor_cover.item_cover") %></li>
|
||||||
|
<li><%= t("pages.sponsor_cover.item_before") %></li>
|
||||||
|
<li><%= t("pages.sponsor_cover.item_club") %></li>
|
||||||
|
</ul>
|
||||||
|
<p class="sponsor-cover__claim"><%= t("pages.sponsor_cover.claim") %></p>
|
||||||
|
<% end %>
|
||||||
|
<% if show_cta %>
|
||||||
|
<%= link_to t("pages.sponsor_cover.cta"), public_prezzi_path, class: "btn btn-outline" %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<div class="sponsor-cover__visual">
|
||||||
|
<%= render "shared/sponsor_cover_mock" %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -10,6 +10,7 @@ de:
|
|||||||
billing: Abrechnung
|
billing: Abrechnung
|
||||||
youtube: YouTube
|
youtube: YouTube
|
||||||
sessions: Sitzungen
|
sessions: Sitzungen
|
||||||
|
stream_concurrency: Konto-Missbrauch
|
||||||
analytics: Analytics
|
analytics: Analytics
|
||||||
costs: Kosten
|
costs: Kosten
|
||||||
stream_nodes: Stream-Knoten
|
stream_nodes: Stream-Knoten
|
||||||
@@ -103,6 +104,12 @@ de:
|
|||||||
warnings: "%{count} Warnung(en)"
|
warnings: "%{count} Warnung(en)"
|
||||||
dashboard_link: Ops-Dashboard
|
dashboard_link: Ops-Dashboard
|
||||||
none_html: "Keine offenen Vorfälle. %{link}"
|
none_html: "Keine offenen Vorfälle. %{link}"
|
||||||
|
concurrency_panel:
|
||||||
|
title: Konto-Missbrauch (zweite Direktübertragung)
|
||||||
|
count: "%{count} Versuch(e) in den letzten 30 Tagen"
|
||||||
|
view_all: Alle ansehen
|
||||||
|
none: Keine Versuche erfasst.
|
||||||
|
none_html: "Keine Versuche einer zweiten Direktübertragung mit demselben Konto. %{link}"
|
||||||
disk:
|
disk:
|
||||||
system_title: Systemfestplatte
|
system_title: Systemfestplatte
|
||||||
free_label: "Frei: %{free} (%{percent}% belegt)"
|
free_label: "Frei: %{free} (%{percent}% belegt)"
|
||||||
@@ -193,6 +200,36 @@ de:
|
|||||||
table:
|
table:
|
||||||
resolved_at: Gelöst
|
resolved_at: Gelöst
|
||||||
none: Kürzlich keine Vorfälle gelöst.
|
none: Kürzlich keine Vorfälle gelöst.
|
||||||
|
stream_concurrency:
|
||||||
|
index:
|
||||||
|
title: Konto-Missbrauch
|
||||||
|
lead: Versuche, mit demselben Konto eine zweite Direktübertragung zu starten, während bereits eine lief (connecting, live, reconnecting oder pausiert).
|
||||||
|
results: "%{shown} von %{total} angezeigt"
|
||||||
|
none: Keine Versuche erfasst.
|
||||||
|
kpi:
|
||||||
|
lookback: Letzte 30 Tage
|
||||||
|
lookback_sub: blockierte Versuche
|
||||||
|
two_devices: Zwei Geräte
|
||||||
|
two_devices_sub: unterschiedliche Handy-Modelle
|
||||||
|
filters:
|
||||||
|
q: Suche
|
||||||
|
q_placeholder: E-Mail, Verein, Spiel…
|
||||||
|
two_devices: Filter
|
||||||
|
two_devices_hint: Nur Versuche von unterschiedlichen Geräten
|
||||||
|
apply: Filtern
|
||||||
|
reset: Zurücksetzen
|
||||||
|
table:
|
||||||
|
when: Wann
|
||||||
|
account: Konto
|
||||||
|
club: Verein
|
||||||
|
occupying: Bereits laufende Direktübertragung
|
||||||
|
attempted: Blockierter Versuch
|
||||||
|
action:
|
||||||
|
start: Start
|
||||||
|
resume: Fortsetzen
|
||||||
|
badge:
|
||||||
|
two_devices: 2 Telefone
|
||||||
|
same_or_unknown: gleiches Gerät / n. v.
|
||||||
clubs:
|
clubs:
|
||||||
index:
|
index:
|
||||||
title: Vereine & Teams
|
title: Vereine & Teams
|
||||||
@@ -224,6 +261,10 @@ de:
|
|||||||
sport: Sportart
|
sport: Sportart
|
||||||
matches_and_details: Spiele & Details
|
matches_and_details: Spiele & Details
|
||||||
replay: Replays
|
replay: Replays
|
||||||
|
concurrency_title: Versuche einer zweiten Direktübertragung
|
||||||
|
concurrency_lead: Dasselbe Konto hat versucht, eine weitere Direktübertragung zu starten, während bereits eine lief.
|
||||||
|
concurrency_none: Keine Versuche für diesen Verein erfasst.
|
||||||
|
concurrency_all: Alle Konto-Missbräuche ansehen
|
||||||
comped:
|
comped:
|
||||||
title: Kostenloses Abonnement
|
title: Kostenloses Abonnement
|
||||||
description: "Sponsor oder Aktion: Vergib Premium Light/Full ohne Stripe-Zahlung. Jederzeit widerrufbar."
|
description: "Sponsor oder Aktion: Vergib Premium Light/Full ohne Stripe-Zahlung. Jederzeit widerrufbar."
|
||||||
@@ -296,6 +337,8 @@ de:
|
|||||||
devices_title: Geräte
|
devices_title: Geräte
|
||||||
events_title: Ereignisse
|
events_title: Ereignisse
|
||||||
events_none: Keine Ereignisse erfasst.
|
events_none: Keine Ereignisse erfasst.
|
||||||
|
concurrency_title: Versuche einer zweiten Direktübertragung
|
||||||
|
concurrency_lead: Dieses Konto hat versucht, eine weitere Direktübertragung zu starten, während diese bereits lief (oder wurde von einer anderen Sitzung desselben Kontos blockiert).
|
||||||
status_label: "Status:"
|
status_label: "Status:"
|
||||||
stop_button: Sitzung beenden
|
stop_button: Sitzung beenden
|
||||||
stop_confirm: "Übertragung beenden? Der RTMP-Pfad wird entfernt und der Status wechselt zu beendet."
|
stop_confirm: "Übertragung beenden? Der RTMP-Pfad wird entfernt und der Status wechselt zu beendet."
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ en:
|
|||||||
billing: Billing
|
billing: Billing
|
||||||
youtube: YouTube
|
youtube: YouTube
|
||||||
sessions: Sessions
|
sessions: Sessions
|
||||||
|
stream_concurrency: Account abuse
|
||||||
analytics: Analytics
|
analytics: Analytics
|
||||||
costs: Costs
|
costs: Costs
|
||||||
stream_nodes: Stream nodes
|
stream_nodes: Stream nodes
|
||||||
@@ -103,6 +104,12 @@ en:
|
|||||||
warnings: "%{count} warning(s)"
|
warnings: "%{count} warning(s)"
|
||||||
dashboard_link: Ops dashboard
|
dashboard_link: Ops dashboard
|
||||||
none_html: "No open incidents. %{link}"
|
none_html: "No open incidents. %{link}"
|
||||||
|
concurrency_panel:
|
||||||
|
title: Account abuse (second live)
|
||||||
|
count: "%{count} attempt(s) in the last 30 days"
|
||||||
|
view_all: View all
|
||||||
|
none: No attempts recorded.
|
||||||
|
none_html: "No second-live attempts from the same account. %{link}"
|
||||||
disk:
|
disk:
|
||||||
system_title: System disk
|
system_title: System disk
|
||||||
free_label: "Free: %{free} (%{percent}% used)"
|
free_label: "Free: %{free} (%{percent}% used)"
|
||||||
@@ -193,6 +200,36 @@ en:
|
|||||||
table:
|
table:
|
||||||
resolved_at: Resolved
|
resolved_at: Resolved
|
||||||
none: No incidents resolved recently.
|
none: No incidents resolved recently.
|
||||||
|
stream_concurrency:
|
||||||
|
index:
|
||||||
|
title: Account abuse
|
||||||
|
lead: Attempts to start a second live with the same account while another was already running (connecting, live, reconnecting or paused).
|
||||||
|
results: "Showing %{shown} of %{total}"
|
||||||
|
none: No attempts recorded.
|
||||||
|
kpi:
|
||||||
|
lookback: Last 30 days
|
||||||
|
lookback_sub: blocked attempts
|
||||||
|
two_devices: Two devices
|
||||||
|
two_devices_sub: different phone models
|
||||||
|
filters:
|
||||||
|
q: Search
|
||||||
|
q_placeholder: Email, club, match…
|
||||||
|
two_devices: Filter
|
||||||
|
two_devices_hint: Only attempts from different devices
|
||||||
|
apply: Filter
|
||||||
|
reset: Reset
|
||||||
|
table:
|
||||||
|
when: When
|
||||||
|
account: Account
|
||||||
|
club: Club
|
||||||
|
occupying: Live already running
|
||||||
|
attempted: Blocked attempt
|
||||||
|
action:
|
||||||
|
start: start
|
||||||
|
resume: resume
|
||||||
|
badge:
|
||||||
|
two_devices: 2 phones
|
||||||
|
same_or_unknown: same device / n.a.
|
||||||
clubs:
|
clubs:
|
||||||
index:
|
index:
|
||||||
title: Clubs & teams
|
title: Clubs & teams
|
||||||
@@ -224,6 +261,10 @@ en:
|
|||||||
sport: Sport
|
sport: Sport
|
||||||
matches_and_details: Matches & details
|
matches_and_details: Matches & details
|
||||||
replay: Replays
|
replay: Replays
|
||||||
|
concurrency_title: Second-live attempts
|
||||||
|
concurrency_lead: Same account tried to start another live while one was already running.
|
||||||
|
concurrency_none: No attempts recorded for this club.
|
||||||
|
concurrency_all: View all account abuse
|
||||||
comped:
|
comped:
|
||||||
title: Complimentary subscription
|
title: Complimentary subscription
|
||||||
description: "Sponsor or promotion: grant Premium Light/Full without a Stripe payment. Revocable at any time."
|
description: "Sponsor or promotion: grant Premium Light/Full without a Stripe payment. Revocable at any time."
|
||||||
@@ -296,6 +337,8 @@ en:
|
|||||||
devices_title: Devices
|
devices_title: Devices
|
||||||
events_title: Events
|
events_title: Events
|
||||||
events_none: No events recorded.
|
events_none: No events recorded.
|
||||||
|
concurrency_title: Second-live attempts
|
||||||
|
concurrency_lead: This account tried to start another live while this one was already running (or was blocked by another session of the same account).
|
||||||
status_label: "Status:"
|
status_label: "Status:"
|
||||||
stop_button: End session
|
stop_button: End session
|
||||||
stop_confirm: "End the broadcast? The RTMP path will be removed and the status will move to ended."
|
stop_confirm: "End the broadcast? The RTMP path will be removed and the status will move to ended."
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ es:
|
|||||||
billing: Facturación
|
billing: Facturación
|
||||||
youtube: YouTube
|
youtube: YouTube
|
||||||
sessions: Sesiones
|
sessions: Sesiones
|
||||||
|
stream_concurrency: Abuso de cuenta
|
||||||
analytics: Analytics
|
analytics: Analytics
|
||||||
costs: Costes
|
costs: Costes
|
||||||
stream_nodes: Nodos stream
|
stream_nodes: Nodos stream
|
||||||
@@ -103,6 +104,12 @@ es:
|
|||||||
warnings: "%{count} aviso(s)"
|
warnings: "%{count} aviso(s)"
|
||||||
dashboard_link: Panel de Ops
|
dashboard_link: Panel de Ops
|
||||||
none_html: "No hay incidencias abiertas. %{link}"
|
none_html: "No hay incidencias abiertas. %{link}"
|
||||||
|
concurrency_panel:
|
||||||
|
title: Abuso de cuenta (segundo directo)
|
||||||
|
count: "%{count} intento(s) en los últimos 30 días"
|
||||||
|
view_all: Ver todos
|
||||||
|
none: No hay intentos registrados.
|
||||||
|
none_html: "Ningún intento de segundo directo con la misma cuenta. %{link}"
|
||||||
disk:
|
disk:
|
||||||
system_title: Disco del sistema
|
system_title: Disco del sistema
|
||||||
free_label: "Libre: %{free} (%{percent}% usado)"
|
free_label: "Libre: %{free} (%{percent}% usado)"
|
||||||
@@ -193,6 +200,36 @@ es:
|
|||||||
table:
|
table:
|
||||||
resolved_at: Resuelta
|
resolved_at: Resuelta
|
||||||
none: No se han resuelto incidencias recientemente.
|
none: No se han resuelto incidencias recientemente.
|
||||||
|
stream_concurrency:
|
||||||
|
index:
|
||||||
|
title: Abuso de cuenta
|
||||||
|
lead: Intentos de iniciar un segundo directo con la misma cuenta mientras ya había otro en curso (connecting, live, reconnecting o en pausa).
|
||||||
|
results: "Mostrando %{shown} de %{total}"
|
||||||
|
none: No hay intentos registrados.
|
||||||
|
kpi:
|
||||||
|
lookback: Últimos 30 días
|
||||||
|
lookback_sub: intentos bloqueados
|
||||||
|
two_devices: Dos dispositivos
|
||||||
|
two_devices_sub: modelos de teléfono distintos
|
||||||
|
filters:
|
||||||
|
q: Buscar
|
||||||
|
q_placeholder: Email, club, partido…
|
||||||
|
two_devices: Filtro
|
||||||
|
two_devices_hint: Solo intentos desde dispositivos distintos
|
||||||
|
apply: Filtrar
|
||||||
|
reset: Restablecer
|
||||||
|
table:
|
||||||
|
when: Cuándo
|
||||||
|
account: Cuenta
|
||||||
|
club: Club
|
||||||
|
occupying: Directo ya en curso
|
||||||
|
attempted: Intento bloqueado
|
||||||
|
action:
|
||||||
|
start: inicio
|
||||||
|
resume: reanudación
|
||||||
|
badge:
|
||||||
|
two_devices: 2 teléfonos
|
||||||
|
same_or_unknown: mismo dispositivo / n. d.
|
||||||
clubs:
|
clubs:
|
||||||
index:
|
index:
|
||||||
title: Clubes y equipos
|
title: Clubes y equipos
|
||||||
@@ -224,6 +261,10 @@ es:
|
|||||||
sport: Deporte
|
sport: Deporte
|
||||||
matches_and_details: Partidos y detalles
|
matches_and_details: Partidos y detalles
|
||||||
replay: Repeticiones
|
replay: Repeticiones
|
||||||
|
concurrency_title: Intentos de segundo directo
|
||||||
|
concurrency_lead: La misma cuenta intentó iniciar otro directo mientras ya había uno en curso.
|
||||||
|
concurrency_none: No hay intentos registrados para este club.
|
||||||
|
concurrency_all: Ver todos los abusos de cuenta
|
||||||
comped:
|
comped:
|
||||||
title: Suscripción de cortesía
|
title: Suscripción de cortesía
|
||||||
description: "Patrocinador o promoción: concede Premium Light/Full sin pago en Stripe. Revocable en cualquier momento."
|
description: "Patrocinador o promoción: concede Premium Light/Full sin pago en Stripe. Revocable en cualquier momento."
|
||||||
@@ -296,6 +337,8 @@ es:
|
|||||||
devices_title: Dispositivos
|
devices_title: Dispositivos
|
||||||
events_title: Eventos
|
events_title: Eventos
|
||||||
events_none: No hay eventos registrados.
|
events_none: No hay eventos registrados.
|
||||||
|
concurrency_title: Intentos de segundo directo
|
||||||
|
concurrency_lead: Esta cuenta intentó iniciar otro directo mientras este ya estaba en curso (o fue bloqueada por otra sesión de la misma cuenta).
|
||||||
status_label: "Estado:"
|
status_label: "Estado:"
|
||||||
stop_button: Finalizar sesión
|
stop_button: Finalizar sesión
|
||||||
stop_confirm: "¿Finalizar la emisión? La ruta RTMP se eliminará y el estado pasará a finalizada."
|
stop_confirm: "¿Finalizar la emisión? La ruta RTMP se eliminará y el estado pasará a finalizada."
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ fr:
|
|||||||
billing: Facturation
|
billing: Facturation
|
||||||
youtube: YouTube
|
youtube: YouTube
|
||||||
sessions: Sessions
|
sessions: Sessions
|
||||||
|
stream_concurrency: Abus de compte
|
||||||
analytics: Analytics
|
analytics: Analytics
|
||||||
costs: Coûts
|
costs: Coûts
|
||||||
stream_nodes: Nœuds stream
|
stream_nodes: Nœuds stream
|
||||||
@@ -103,6 +104,12 @@ fr:
|
|||||||
warnings: "%{count} avertissement(s)"
|
warnings: "%{count} avertissement(s)"
|
||||||
dashboard_link: Tableau de bord Ops
|
dashboard_link: Tableau de bord Ops
|
||||||
none_html: "Aucun incident ouvert. %{link}"
|
none_html: "Aucun incident ouvert. %{link}"
|
||||||
|
concurrency_panel:
|
||||||
|
title: Abus de compte (deuxième direct)
|
||||||
|
count: "%{count} tentative(s) sur les 30 derniers jours"
|
||||||
|
view_all: Voir tout
|
||||||
|
none: Aucune tentative enregistrée.
|
||||||
|
none_html: "Aucune tentative de second direct avec le même compte. %{link}"
|
||||||
disk:
|
disk:
|
||||||
system_title: Disque système
|
system_title: Disque système
|
||||||
free_label: "Libre : %{free} (%{percent}% utilisé)"
|
free_label: "Libre : %{free} (%{percent}% utilisé)"
|
||||||
@@ -193,6 +200,36 @@ fr:
|
|||||||
table:
|
table:
|
||||||
resolved_at: Résolu
|
resolved_at: Résolu
|
||||||
none: Aucun incident résolu récemment.
|
none: Aucun incident résolu récemment.
|
||||||
|
stream_concurrency:
|
||||||
|
index:
|
||||||
|
title: Abus de compte
|
||||||
|
lead: Tentatives de démarrer un second direct avec le même compte alors qu’un autre était déjà en cours (connecting, live, reconnecting ou en pause).
|
||||||
|
results: "%{shown} sur %{total} affichés"
|
||||||
|
none: Aucune tentative enregistrée.
|
||||||
|
kpi:
|
||||||
|
lookback: 30 derniers jours
|
||||||
|
lookback_sub: tentatives bloquées
|
||||||
|
two_devices: Deux appareils
|
||||||
|
two_devices_sub: modèles de téléphone différents
|
||||||
|
filters:
|
||||||
|
q: Rechercher
|
||||||
|
q_placeholder: E-mail, club, match…
|
||||||
|
two_devices: Filtre
|
||||||
|
two_devices_hint: Uniquement les tentatives depuis des appareils différents
|
||||||
|
apply: Filtrer
|
||||||
|
reset: Réinitialiser
|
||||||
|
table:
|
||||||
|
when: Quand
|
||||||
|
account: Compte
|
||||||
|
club: Club
|
||||||
|
occupying: Direct déjà en cours
|
||||||
|
attempted: Tentative bloquée
|
||||||
|
action:
|
||||||
|
start: démarrage
|
||||||
|
resume: reprise
|
||||||
|
badge:
|
||||||
|
two_devices: 2 téléphones
|
||||||
|
same_or_unknown: même appareil / n. d.
|
||||||
clubs:
|
clubs:
|
||||||
index:
|
index:
|
||||||
title: Clubs et équipes
|
title: Clubs et équipes
|
||||||
@@ -224,6 +261,10 @@ fr:
|
|||||||
sport: Sport
|
sport: Sport
|
||||||
matches_and_details: Matchs et détails
|
matches_and_details: Matchs et détails
|
||||||
replay: Replays
|
replay: Replays
|
||||||
|
concurrency_title: Tentatives de second direct
|
||||||
|
concurrency_lead: Le même compte a tenté de démarrer un autre direct alors qu’un était déjà en cours.
|
||||||
|
concurrency_none: Aucune tentative enregistrée pour ce club.
|
||||||
|
concurrency_all: Voir tous les abus de compte
|
||||||
comped:
|
comped:
|
||||||
title: Abonnement offert
|
title: Abonnement offert
|
||||||
description: "Sponsor ou promotion : accordez Premium Light/Full sans paiement Stripe. Révocable à tout moment."
|
description: "Sponsor ou promotion : accordez Premium Light/Full sans paiement Stripe. Révocable à tout moment."
|
||||||
@@ -296,6 +337,8 @@ fr:
|
|||||||
devices_title: Appareils
|
devices_title: Appareils
|
||||||
events_title: Événements
|
events_title: Événements
|
||||||
events_none: Aucun événement enregistré.
|
events_none: Aucun événement enregistré.
|
||||||
|
concurrency_title: Tentatives de second direct
|
||||||
|
concurrency_lead: Ce compte a tenté de démarrer un autre direct alors que celui-ci était déjà en cours (ou a été bloqué par une autre session du même compte).
|
||||||
status_label: "Statut :"
|
status_label: "Statut :"
|
||||||
stop_button: Terminer la session
|
stop_button: Terminer la session
|
||||||
stop_confirm: "Terminer la diffusion ? Le chemin RTMP sera supprimé et le statut passera à terminé."
|
stop_confirm: "Terminer la diffusion ? Le chemin RTMP sera supprimé et le statut passera à terminé."
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ it:
|
|||||||
billing: Fatturazione
|
billing: Fatturazione
|
||||||
youtube: YouTube
|
youtube: YouTube
|
||||||
sessions: Sessioni
|
sessions: Sessioni
|
||||||
|
stream_concurrency: Abusi account
|
||||||
analytics: Analytics
|
analytics: Analytics
|
||||||
costs: Costi
|
costs: Costi
|
||||||
stream_nodes: Nodi stream
|
stream_nodes: Nodi stream
|
||||||
@@ -107,6 +108,12 @@ it:
|
|||||||
warnings: "%{count} warning"
|
warnings: "%{count} warning"
|
||||||
dashboard_link: Dashboard Ops
|
dashboard_link: Dashboard Ops
|
||||||
none_html: "Nessun incidente aperto. %{link}"
|
none_html: "Nessun incidente aperto. %{link}"
|
||||||
|
concurrency_panel:
|
||||||
|
title: Abusi account (seconda diretta)
|
||||||
|
count: "%{count} tentativo/i negli ultimi 30 giorni"
|
||||||
|
view_all: Vedi tutti
|
||||||
|
none: Nessun tentativo registrato.
|
||||||
|
none_html: "Nessun tentativo di seconda diretta dallo stesso account. %{link}"
|
||||||
disk:
|
disk:
|
||||||
system_title: Disco sistema
|
system_title: Disco sistema
|
||||||
free_label: "Libero: %{free} (%{percent}% usato)"
|
free_label: "Libero: %{free} (%{percent}% usato)"
|
||||||
@@ -197,6 +204,36 @@ it:
|
|||||||
table:
|
table:
|
||||||
resolved_at: Risolto
|
resolved_at: Risolto
|
||||||
none: Nessun incidente risolto di recente.
|
none: Nessun incidente risolto di recente.
|
||||||
|
stream_concurrency:
|
||||||
|
index:
|
||||||
|
title: Abusi account
|
||||||
|
lead: Tentativi di avviare una seconda diretta con lo stesso account mentre un’altra era già in corso (connecting, live, reconnecting o in pausa).
|
||||||
|
results: "Mostrate %{shown} di %{total}"
|
||||||
|
none: Nessun tentativo registrato.
|
||||||
|
kpi:
|
||||||
|
lookback: Ultimi 30 giorni
|
||||||
|
lookback_sub: tentativi bloccati
|
||||||
|
two_devices: Due dispositivi
|
||||||
|
two_devices_sub: modelli telefono diversi
|
||||||
|
filters:
|
||||||
|
q: Cerca
|
||||||
|
q_placeholder: Email, società, partita…
|
||||||
|
two_devices: Filtro
|
||||||
|
two_devices_hint: Solo tentativi da dispositivi diversi
|
||||||
|
apply: Filtra
|
||||||
|
reset: Reset
|
||||||
|
table:
|
||||||
|
when: Quando
|
||||||
|
account: Account
|
||||||
|
club: Società
|
||||||
|
occupying: Diretta già in corso
|
||||||
|
attempted: Tentativo bloccato
|
||||||
|
action:
|
||||||
|
start: avvio
|
||||||
|
resume: ripresa
|
||||||
|
badge:
|
||||||
|
two_devices: 2 telefoni
|
||||||
|
same_or_unknown: stesso device / n.d.
|
||||||
clubs:
|
clubs:
|
||||||
index:
|
index:
|
||||||
title: Società e squadre
|
title: Società e squadre
|
||||||
@@ -229,6 +266,10 @@ it:
|
|||||||
sport: Sport
|
sport: Sport
|
||||||
matches_and_details: Partite e dettagli
|
matches_and_details: Partite e dettagli
|
||||||
replay: Replay
|
replay: Replay
|
||||||
|
concurrency_title: Tentativi di seconda diretta
|
||||||
|
concurrency_lead: Stesso account che ha provato ad avviare un’altra diretta mentre ne era già in corso una.
|
||||||
|
concurrency_none: Nessun tentativo registrato per questa società.
|
||||||
|
concurrency_all: Vedi tutti gli abusi account
|
||||||
comped:
|
comped:
|
||||||
title: Abbonamento omaggio
|
title: Abbonamento omaggio
|
||||||
description: "Sponsor o promozione: assegna Premium Light/Full senza pagamento Stripe. Revocabile in qualsiasi momento."
|
description: "Sponsor o promozione: assegna Premium Light/Full senza pagamento Stripe. Revocabile in qualsiasi momento."
|
||||||
@@ -317,6 +358,8 @@ it:
|
|||||||
devices_title: Dispositivi
|
devices_title: Dispositivi
|
||||||
events_title: Eventi
|
events_title: Eventi
|
||||||
events_none: Nessun evento registrato.
|
events_none: Nessun evento registrato.
|
||||||
|
concurrency_title: Tentativi di seconda diretta
|
||||||
|
concurrency_lead: Questo account ha provato ad avviare un’altra diretta mentre questa era già in corso (o è stato bloccato da un’altra sessione dello stesso account).
|
||||||
status_label: "Stato:"
|
status_label: "Stato:"
|
||||||
stop_button: Termina sessione
|
stop_button: Termina sessione
|
||||||
stop_confirm: "Terminare la trasmissione? Il path RTMP verrà rimosso e lo stato passerà a ended."
|
stop_confirm: "Terminare la trasmissione? Il path RTMP verrà rimosso e lo stato passerà a ended."
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
de:
|
de:
|
||||||
|
api:
|
||||||
|
errors:
|
||||||
|
user_concurrent_stream: "Mit diesem Konto läuft bereits eine Direktübertragung. Beende sie, bevor du eine weitere startest."
|
||||||
password_policy:
|
password_policy:
|
||||||
hint: "Mindestens 8 Zeichen, mit mindestens 3 aus: Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen."
|
hint: "Mindestens 8 Zeichen, mit mindestens 3 aus: Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen."
|
||||||
activerecord:
|
activerecord:
|
||||||
@@ -77,10 +80,10 @@ de:
|
|||||||
title: "Verein registrieren — Match Live TV"
|
title: "Verein registrieren — Match Live TV"
|
||||||
meta_description: Erstelle deinen Sportverein und das erste Team auf Match Live TV.
|
meta_description: Erstelle deinen Sportverein und das erste Team auf Match Live TV.
|
||||||
heading: Dein Verein
|
heading: Dein Verein
|
||||||
lead: "Registriere den Verein: du kannst später weitere Teams hinzufügen (U13, U15, Serie C…)."
|
lead: "Registriere den Verein: du kannst später weitere Teams hinzufügen (U13, U15, erste Mannschaft…)."
|
||||||
section_club: Verein
|
section_club: Verein
|
||||||
name_label: Vereinsname / Club
|
name_label: Vereinsname / Club
|
||||||
name_placeholder: "z. B. Crazy Volley Rozzano"
|
name_placeholder: "z. B. Team MLTV"
|
||||||
section_first_team: Erstes Team
|
section_first_team: Erstes Team
|
||||||
default_first_team_name: Erstes Team
|
default_first_team_name: Erstes Team
|
||||||
first_team_name_label: Teamname
|
first_team_name_label: Teamname
|
||||||
@@ -159,7 +162,7 @@ de:
|
|||||||
heading: Neues Team
|
heading: Neues Team
|
||||||
club_label: "Verein:"
|
club_label: "Verein:"
|
||||||
name_label: Teamname
|
name_label: Teamname
|
||||||
name_placeholder: "z. B. Under 13, Serie C"
|
name_placeholder: "z. B. U15 männlich"
|
||||||
branding_legend: Branding-Überschreibung (optional)
|
branding_legend: Branding-Überschreibung (optional)
|
||||||
submit: Team hinzufügen
|
submit: Team hinzufügen
|
||||||
invite:
|
invite:
|
||||||
@@ -235,7 +238,7 @@ de:
|
|||||||
matches:
|
matches:
|
||||||
back_to_list: "← Spieleliste"
|
back_to_list: "← Spieleliste"
|
||||||
opponent_label: Gegner
|
opponent_label: Gegner
|
||||||
opponent_placeholder: "z. B. Volley Milano"
|
opponent_placeholder: "z. B. Team Guest"
|
||||||
location_label: "Ort (optional)"
|
location_label: "Ort (optional)"
|
||||||
location_placeholder: "Halle, Stadt"
|
location_placeholder: "Halle, Stadt"
|
||||||
datetime_label: Datum und Uhrzeit
|
datetime_label: Datum und Uhrzeit
|
||||||
@@ -361,7 +364,7 @@ de:
|
|||||||
legend_hint: "Alle markierten Felder sind erforderlich, um einen Premium-Tarif zu abonnieren und Rechnungen auszustellen. Vereine benötigen eine USt-IdNr., Privatpersonen eine Steuernummer. Du brauchst entweder SDI oder eine zertifizierte E-Mail (PEC). Die Zahlungen werden weiterhin sicher von Stripe abgewickelt."
|
legend_hint: "Alle markierten Felder sind erforderlich, um einen Premium-Tarif zu abonnieren und Rechnungen auszustellen. Vereine benötigen eine USt-IdNr., Privatpersonen eine Steuernummer. Du brauchst entweder SDI oder eine zertifizierte E-Mail (PEC). Die Zahlungen werden weiterhin sicher von Stripe abgewickelt."
|
||||||
entity_type_label: "Art des Rechnungsempfängers *"
|
entity_type_label: "Art des Rechnungsempfängers *"
|
||||||
legal_name_label: "Firmenname / Vor- und Nachname *"
|
legal_name_label: "Firmenname / Vor- und Nachname *"
|
||||||
legal_name_placeholder: "z. B. ASD Tigers Volley"
|
legal_name_placeholder: "z. B. Team MLTV"
|
||||||
vat_number_label: "USt-IdNr. * (Verein)"
|
vat_number_label: "USt-IdNr. * (Verein)"
|
||||||
fiscal_code_label: "Steuernummer * (Privatperson)"
|
fiscal_code_label: "Steuernummer * (Privatperson)"
|
||||||
fiscal_code_placeholder: RSSMRA80A01H501U
|
fiscal_code_placeholder: RSSMRA80A01H501U
|
||||||
@@ -738,7 +741,7 @@ de:
|
|||||||
replay_archive_link: "Vergangene Live-Übertragungen — Wiederholungsarchiv"
|
replay_archive_link: "Vergangene Live-Übertragungen — Wiederholungsarchiv"
|
||||||
schedule_match_link: "Spiel planen"
|
schedule_match_link: "Spiel planen"
|
||||||
search_placeholder_club: "Verein, Team, Gegner oder Ort suchen…"
|
search_placeholder_club: "Verein, Team, Gegner oder Ort suchen…"
|
||||||
search_placeholder_default: "Z. B. Crazy Volley, Serie D, Gegner…"
|
search_placeholder_default: "Z. B. Team MLTV, Team Guest…"
|
||||||
search_aria_label: "Team suchen"
|
search_aria_label: "Team suchen"
|
||||||
search_button: "Suchen"
|
search_button: "Suchen"
|
||||||
reset_link: "Zurücksetzen"
|
reset_link: "Zurücksetzen"
|
||||||
@@ -772,7 +775,7 @@ de:
|
|||||||
empty_hero_cta_features: "Erfahre, wie es funktioniert"
|
empty_hero_cta_features: "Erfahre, wie es funktioniert"
|
||||||
demo_aria_label: "Beispiel einer aktiven Live-Übertragung"
|
demo_aria_label: "Beispiel einer aktiven Live-Übertragung"
|
||||||
demo_label: "Beispiel — so sieht eine aktive Übertragung aus"
|
demo_label: "Beispiel — so sieht eine aktive Übertragung aus"
|
||||||
demo_meta: "PalaTigers · Match Live TV"
|
demo_meta: "Pala MLTV · Match Live TV"
|
||||||
demo_sets: "Satz 2 · Sätze gewonnen 1-0"
|
demo_sets: "Satz 2 · Sätze gewonnen 1-0"
|
||||||
show:
|
show:
|
||||||
back_to_all: "← Alle Live-Übertragungen"
|
back_to_all: "← Alle Live-Übertragungen"
|
||||||
@@ -805,7 +808,7 @@ de:
|
|||||||
back_to_live: "← Live-Übertragungen"
|
back_to_live: "← Live-Übertragungen"
|
||||||
title: "Vergangene Live-Übertragungen"
|
title: "Vergangene Live-Übertragungen"
|
||||||
hint: "Öffentliche Wiederholungen von Sportvereinen — betrifft bereits übertragene Spiele."
|
hint: "Öffentliche Wiederholungen von Sportvereinen — betrifft bereits übertragene Spiele."
|
||||||
search_placeholder: "Z. B. Tigers Volley, Gegner, Verein, Ort…"
|
search_placeholder: "Z. B. Team MLTV, Team Guest…"
|
||||||
search_aria_label: "Wiederholung suchen"
|
search_aria_label: "Wiederholung suchen"
|
||||||
search_button: "Suchen"
|
search_button: "Suchen"
|
||||||
reset_link: "Zurücksetzen"
|
reset_link: "Zurücksetzen"
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
en:
|
en:
|
||||||
|
api:
|
||||||
|
errors:
|
||||||
|
user_concurrent_stream: "You already have a live stream running on this account. Stop it before starting another."
|
||||||
password_policy:
|
password_policy:
|
||||||
hint: "At least 8 characters, including 3 of: lowercase, uppercase, numbers and symbols."
|
hint: "At least 8 characters, including 3 of: lowercase, uppercase, numbers and symbols."
|
||||||
activerecord:
|
activerecord:
|
||||||
@@ -82,10 +85,10 @@ en:
|
|||||||
title: "Register a club — Match Live TV"
|
title: "Register a club — Match Live TV"
|
||||||
meta_description: Create your sports club and its first team on Match Live TV.
|
meta_description: Create your sports club and its first team on Match Live TV.
|
||||||
heading: Your club
|
heading: Your club
|
||||||
lead: "Register the club: you'll be able to add more teams (Under 13, Under 15, Serie C…)."
|
lead: "Register the club: you'll be able to add more teams (Under 13, Under 15, first team…)."
|
||||||
section_club: Club
|
section_club: Club
|
||||||
name_label: Club name
|
name_label: Club name
|
||||||
name_placeholder: "e.g. Crazy Volley Rozzano"
|
name_placeholder: "e.g. Team MLTV"
|
||||||
section_first_team: First team
|
section_first_team: First team
|
||||||
default_first_team_name: First team
|
default_first_team_name: First team
|
||||||
first_team_name_label: Team name
|
first_team_name_label: Team name
|
||||||
@@ -164,7 +167,7 @@ en:
|
|||||||
heading: New team
|
heading: New team
|
||||||
club_label: "Club:"
|
club_label: "Club:"
|
||||||
name_label: Team name
|
name_label: Team name
|
||||||
name_placeholder: "e.g. Under 13, Serie C"
|
name_placeholder: "e.g. U15 boys"
|
||||||
branding_legend: Branding override (optional)
|
branding_legend: Branding override (optional)
|
||||||
submit: Add team
|
submit: Add team
|
||||||
invite:
|
invite:
|
||||||
@@ -240,7 +243,7 @@ en:
|
|||||||
matches:
|
matches:
|
||||||
back_to_list: "← Match list"
|
back_to_list: "← Match list"
|
||||||
opponent_label: Opponent
|
opponent_label: Opponent
|
||||||
opponent_placeholder: "e.g. Volley Milano"
|
opponent_placeholder: "e.g. Team Guest"
|
||||||
location_label: "Location (optional)"
|
location_label: "Location (optional)"
|
||||||
location_placeholder: "Gym, city"
|
location_placeholder: "Gym, city"
|
||||||
datetime_label: Date and time
|
datetime_label: Date and time
|
||||||
@@ -366,7 +369,7 @@ en:
|
|||||||
legend_hint: "All marked fields are required to subscribe to a premium plan and to issue invoices. Clubs need a VAT number; private individuals need a fiscal code. You need either SDI or certified email (PEC). Payments remain securely handled by Stripe."
|
legend_hint: "All marked fields are required to subscribe to a premium plan and to issue invoices. Clubs need a VAT number; private individuals need a fiscal code. You need either SDI or certified email (PEC). Payments remain securely handled by Stripe."
|
||||||
entity_type_label: "Billing entity type *"
|
entity_type_label: "Billing entity type *"
|
||||||
legal_name_label: "Legal name / full name *"
|
legal_name_label: "Legal name / full name *"
|
||||||
legal_name_placeholder: "e.g. ASD Tigers Volley"
|
legal_name_placeholder: "e.g. Team MLTV"
|
||||||
vat_number_label: "VAT number * (club)"
|
vat_number_label: "VAT number * (club)"
|
||||||
fiscal_code_label: "Fiscal code * (private individual)"
|
fiscal_code_label: "Fiscal code * (private individual)"
|
||||||
fiscal_code_placeholder: RSSMRA80A01H501U
|
fiscal_code_placeholder: RSSMRA80A01H501U
|
||||||
@@ -743,7 +746,7 @@ en:
|
|||||||
replay_archive_link: "Past live streams — replay archive"
|
replay_archive_link: "Past live streams — replay archive"
|
||||||
schedule_match_link: "Schedule a match"
|
schedule_match_link: "Schedule a match"
|
||||||
search_placeholder_club: "Search club, team, opponent or venue…"
|
search_placeholder_club: "Search club, team, opponent or venue…"
|
||||||
search_placeholder_default: "E.g. Crazy Volley, Serie D, opponent…"
|
search_placeholder_default: "E.g. Team MLTV, Team Guest…"
|
||||||
search_aria_label: "Search team"
|
search_aria_label: "Search team"
|
||||||
search_button: "Search"
|
search_button: "Search"
|
||||||
reset_link: "Reset"
|
reset_link: "Reset"
|
||||||
@@ -777,7 +780,7 @@ en:
|
|||||||
empty_hero_cta_features: "See how it works"
|
empty_hero_cta_features: "See how it works"
|
||||||
demo_aria_label: "Example of an active live stream"
|
demo_aria_label: "Example of an active live stream"
|
||||||
demo_label: "Example — this is how an active stream looks"
|
demo_label: "Example — this is how an active stream looks"
|
||||||
demo_meta: "PalaTigers · Match Live TV"
|
demo_meta: "Pala MLTV · Match Live TV"
|
||||||
demo_sets: "Set 2 · Sets won 1-0"
|
demo_sets: "Set 2 · Sets won 1-0"
|
||||||
show:
|
show:
|
||||||
back_to_all: "← All live streams"
|
back_to_all: "← All live streams"
|
||||||
@@ -810,7 +813,7 @@ en:
|
|||||||
back_to_live: "← Live streams"
|
back_to_live: "← Live streams"
|
||||||
title: "Past live streams"
|
title: "Past live streams"
|
||||||
hint: "Public replays from sports clubs — covers matches already broadcast."
|
hint: "Public replays from sports clubs — covers matches already broadcast."
|
||||||
search_placeholder: "E.g. Tigers Volley, opponent, club, venue…"
|
search_placeholder: "E.g. Team MLTV, Team Guest…"
|
||||||
search_aria_label: "Search replays"
|
search_aria_label: "Search replays"
|
||||||
search_button: "Search"
|
search_button: "Search"
|
||||||
reset_link: "Reset"
|
reset_link: "Reset"
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
es:
|
es:
|
||||||
|
api:
|
||||||
|
errors:
|
||||||
|
user_concurrent_stream: "Ya tienes un directo en curso con esta cuenta. Ciérralo antes de iniciar otro."
|
||||||
password_policy:
|
password_policy:
|
||||||
hint: "Mínimo 8 caracteres, con al menos 3 entre: minúsculas, mayúsculas, números y símbolos."
|
hint: "Mínimo 8 caracteres, con al menos 3 entre: minúsculas, mayúsculas, números y símbolos."
|
||||||
activerecord:
|
activerecord:
|
||||||
@@ -77,10 +80,10 @@ es:
|
|||||||
title: "Registrar un club — Match Live TV"
|
title: "Registrar un club — Match Live TV"
|
||||||
meta_description: Crea tu club deportivo y su primer equipo en Match Live TV.
|
meta_description: Crea tu club deportivo y su primer equipo en Match Live TV.
|
||||||
heading: Tu club
|
heading: Tu club
|
||||||
lead: "Registra el club: podrás añadir más equipos (Sub-13, Sub-15, Serie C…)."
|
lead: "Registra el club: podrás añadir más equipos (Sub-13, Sub-15, primer equipo…)."
|
||||||
section_club: Club
|
section_club: Club
|
||||||
name_label: Nombre del club
|
name_label: Nombre del club
|
||||||
name_placeholder: "ej. Crazy Volley Rozzano"
|
name_placeholder: "ej. Team MLTV"
|
||||||
section_first_team: Primer equipo
|
section_first_team: Primer equipo
|
||||||
default_first_team_name: Primer equipo
|
default_first_team_name: Primer equipo
|
||||||
first_team_name_label: Nombre del equipo
|
first_team_name_label: Nombre del equipo
|
||||||
@@ -159,7 +162,7 @@ es:
|
|||||||
heading: Nuevo equipo
|
heading: Nuevo equipo
|
||||||
club_label: "Club:"
|
club_label: "Club:"
|
||||||
name_label: Nombre del equipo
|
name_label: Nombre del equipo
|
||||||
name_placeholder: "ej. Sub-13, Serie C"
|
name_placeholder: "ej. U15 masculino"
|
||||||
branding_legend: Personalización de imagen de marca (opcional)
|
branding_legend: Personalización de imagen de marca (opcional)
|
||||||
submit: Añadir equipo
|
submit: Añadir equipo
|
||||||
invite:
|
invite:
|
||||||
@@ -235,7 +238,7 @@ es:
|
|||||||
matches:
|
matches:
|
||||||
back_to_list: "← Lista de partidos"
|
back_to_list: "← Lista de partidos"
|
||||||
opponent_label: Rival
|
opponent_label: Rival
|
||||||
opponent_placeholder: "ej. Volley Milano"
|
opponent_placeholder: "ej. Team Guest"
|
||||||
location_label: "Lugar (opcional)"
|
location_label: "Lugar (opcional)"
|
||||||
location_placeholder: "Pabellón, ciudad"
|
location_placeholder: "Pabellón, ciudad"
|
||||||
datetime_label: Fecha y hora
|
datetime_label: Fecha y hora
|
||||||
@@ -361,7 +364,7 @@ es:
|
|||||||
legend_hint: "Todos los campos marcados son obligatorios para suscribirte a un plan premium y para emitir facturas. Los clubes necesitan NIF/CIF; las personas físicas, el código fiscal. Necesitas SDI o correo certificado (PEC). Los pagos siguen gestionados de forma segura por Stripe."
|
legend_hint: "Todos los campos marcados son obligatorios para suscribirte a un plan premium y para emitir facturas. Los clubes necesitan NIF/CIF; las personas físicas, el código fiscal. Necesitas SDI o correo certificado (PEC). Los pagos siguen gestionados de forma segura por Stripe."
|
||||||
entity_type_label: "Tipo de titular *"
|
entity_type_label: "Tipo de titular *"
|
||||||
legal_name_label: "Razón social / nombre y apellidos *"
|
legal_name_label: "Razón social / nombre y apellidos *"
|
||||||
legal_name_placeholder: "ej. ASD Tigers Volley"
|
legal_name_placeholder: "ej. Team MLTV"
|
||||||
vat_number_label: "NIF/CIF * (club)"
|
vat_number_label: "NIF/CIF * (club)"
|
||||||
fiscal_code_label: "Código fiscal * (persona física)"
|
fiscal_code_label: "Código fiscal * (persona física)"
|
||||||
fiscal_code_placeholder: RSSMRA80A01H501U
|
fiscal_code_placeholder: RSSMRA80A01H501U
|
||||||
@@ -738,7 +741,7 @@ es:
|
|||||||
replay_archive_link: "Directos pasados — archivo de repeticiones"
|
replay_archive_link: "Directos pasados — archivo de repeticiones"
|
||||||
schedule_match_link: "Programar partido"
|
schedule_match_link: "Programar partido"
|
||||||
search_placeholder_club: "Buscar club, equipo, rival o lugar…"
|
search_placeholder_club: "Buscar club, equipo, rival o lugar…"
|
||||||
search_placeholder_default: "Ej. Crazy Volley, Serie D, rival…"
|
search_placeholder_default: "Ej. Team MLTV, Team Guest…"
|
||||||
search_aria_label: "Buscar equipo"
|
search_aria_label: "Buscar equipo"
|
||||||
search_button: "Buscar"
|
search_button: "Buscar"
|
||||||
reset_link: "Restablecer"
|
reset_link: "Restablecer"
|
||||||
@@ -772,7 +775,7 @@ es:
|
|||||||
empty_hero_cta_features: "Descubre cómo funciona"
|
empty_hero_cta_features: "Descubre cómo funciona"
|
||||||
demo_aria_label: "Ejemplo de un directo activo"
|
demo_aria_label: "Ejemplo de un directo activo"
|
||||||
demo_label: "Ejemplo — así se ve un directo activo"
|
demo_label: "Ejemplo — así se ve un directo activo"
|
||||||
demo_meta: "PalaTigers · Match Live TV"
|
demo_meta: "Pala MLTV · Match Live TV"
|
||||||
demo_sets: "Set 2 · Sets ganados 1-0"
|
demo_sets: "Set 2 · Sets ganados 1-0"
|
||||||
show:
|
show:
|
||||||
back_to_all: "← Todos los directos"
|
back_to_all: "← Todos los directos"
|
||||||
@@ -805,7 +808,7 @@ es:
|
|||||||
back_to_live: "← Directos en curso"
|
back_to_live: "← Directos en curso"
|
||||||
title: "Directos pasados"
|
title: "Directos pasados"
|
||||||
hint: "Repeticiones públicas de los clubes deportivos — corresponde a partidos ya transmitidos."
|
hint: "Repeticiones públicas de los clubes deportivos — corresponde a partidos ya transmitidos."
|
||||||
search_placeholder: "Ej. Tigers Volley, rival, club, lugar…"
|
search_placeholder: "Ej. Team MLTV, Team Guest…"
|
||||||
search_aria_label: "Buscar repetición"
|
search_aria_label: "Buscar repetición"
|
||||||
search_button: "Buscar"
|
search_button: "Buscar"
|
||||||
reset_link: "Restablecer"
|
reset_link: "Restablecer"
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
fr:
|
fr:
|
||||||
|
api:
|
||||||
|
errors:
|
||||||
|
user_concurrent_stream: "Un direct est déjà en cours avec ce compte. Arrêtez-le avant d’en démarrer un autre."
|
||||||
password_policy:
|
password_policy:
|
||||||
hint: "Au moins 8 caractères, avec au moins 3 parmi : minuscules, majuscules, chiffres et symboles."
|
hint: "Au moins 8 caractères, avec au moins 3 parmi : minuscules, majuscules, chiffres et symboles."
|
||||||
activerecord:
|
activerecord:
|
||||||
@@ -77,10 +80,10 @@ fr:
|
|||||||
title: "Inscrire un club — Match Live TV"
|
title: "Inscrire un club — Match Live TV"
|
||||||
meta_description: Crée ton club sportif et sa première équipe sur Match Live TV.
|
meta_description: Crée ton club sportif et sa première équipe sur Match Live TV.
|
||||||
heading: Ton club
|
heading: Ton club
|
||||||
lead: "Inscris le club : tu pourras ajouter d'autres équipes (Under 13, Under 15, Serie C…)."
|
lead: "Inscris le club : tu pourras ajouter d'autres équipes (Under 13, Under 15, première équipe…)."
|
||||||
section_club: Club
|
section_club: Club
|
||||||
name_label: Nom du club
|
name_label: Nom du club
|
||||||
name_placeholder: "ex. Crazy Volley Rozzano"
|
name_placeholder: "ex. Team MLTV"
|
||||||
section_first_team: Première équipe
|
section_first_team: Première équipe
|
||||||
default_first_team_name: Première équipe
|
default_first_team_name: Première équipe
|
||||||
first_team_name_label: Nom de l'équipe
|
first_team_name_label: Nom de l'équipe
|
||||||
@@ -159,7 +162,7 @@ fr:
|
|||||||
heading: Nouvelle équipe
|
heading: Nouvelle équipe
|
||||||
club_label: "Club :"
|
club_label: "Club :"
|
||||||
name_label: Nom de l'équipe
|
name_label: Nom de l'équipe
|
||||||
name_placeholder: "ex. Under 13, Serie C"
|
name_placeholder: "ex. U15 masculin"
|
||||||
branding_legend: Personnalisation de l'image de marque (facultatif)
|
branding_legend: Personnalisation de l'image de marque (facultatif)
|
||||||
submit: Ajouter l'équipe
|
submit: Ajouter l'équipe
|
||||||
invite:
|
invite:
|
||||||
@@ -235,7 +238,7 @@ fr:
|
|||||||
matches:
|
matches:
|
||||||
back_to_list: "← Liste des matchs"
|
back_to_list: "← Liste des matchs"
|
||||||
opponent_label: Adversaire
|
opponent_label: Adversaire
|
||||||
opponent_placeholder: "ex. Volley Milano"
|
opponent_placeholder: "ex. Team Guest"
|
||||||
location_label: "Lieu (facultatif)"
|
location_label: "Lieu (facultatif)"
|
||||||
location_placeholder: "Gymnase, ville"
|
location_placeholder: "Gymnase, ville"
|
||||||
datetime_label: Date et heure
|
datetime_label: Date et heure
|
||||||
@@ -361,7 +364,7 @@ fr:
|
|||||||
legend_hint: "Tous les champs marqués sont obligatoires pour souscrire à un forfait premium et pour émettre les factures. Les clubs ont besoin d'un numéro de TVA ; les personnes physiques d'un code fiscal. Il faut le SDI ou le PEC. Les paiements restent gérés en toute sécurité par Stripe."
|
legend_hint: "Tous les champs marqués sont obligatoires pour souscrire à un forfait premium et pour émettre les factures. Les clubs ont besoin d'un numéro de TVA ; les personnes physiques d'un code fiscal. Il faut le SDI ou le PEC. Les paiements restent gérés en toute sécurité par Stripe."
|
||||||
entity_type_label: "Type de titulaire *"
|
entity_type_label: "Type de titulaire *"
|
||||||
legal_name_label: "Raison sociale / nom et prénom *"
|
legal_name_label: "Raison sociale / nom et prénom *"
|
||||||
legal_name_placeholder: "ex. ASD Tigers Volley"
|
legal_name_placeholder: "ex. Team MLTV"
|
||||||
vat_number_label: "Numéro de TVA * (club)"
|
vat_number_label: "Numéro de TVA * (club)"
|
||||||
fiscal_code_label: "Code fiscal * (personne physique)"
|
fiscal_code_label: "Code fiscal * (personne physique)"
|
||||||
fiscal_code_placeholder: RSSMRA80A01H501U
|
fiscal_code_placeholder: RSSMRA80A01H501U
|
||||||
@@ -738,7 +741,7 @@ fr:
|
|||||||
replay_archive_link: "Directs passés — archive des replays"
|
replay_archive_link: "Directs passés — archive des replays"
|
||||||
schedule_match_link: "Programmer un match"
|
schedule_match_link: "Programmer un match"
|
||||||
search_placeholder_club: "Rechercher club, équipe, adversaire ou lieu…"
|
search_placeholder_club: "Rechercher club, équipe, adversaire ou lieu…"
|
||||||
search_placeholder_default: "Ex. Crazy Volley, Série D, adversaire…"
|
search_placeholder_default: "Ex. Team MLTV, Team Guest…"
|
||||||
search_aria_label: "Rechercher une équipe"
|
search_aria_label: "Rechercher une équipe"
|
||||||
search_button: "Rechercher"
|
search_button: "Rechercher"
|
||||||
reset_link: "Réinitialiser"
|
reset_link: "Réinitialiser"
|
||||||
@@ -772,7 +775,7 @@ fr:
|
|||||||
empty_hero_cta_features: "Découvrez comment ça marche"
|
empty_hero_cta_features: "Découvrez comment ça marche"
|
||||||
demo_aria_label: "Exemple de direct actif"
|
demo_aria_label: "Exemple de direct actif"
|
||||||
demo_label: "Exemple — voici à quoi ressemble un direct actif"
|
demo_label: "Exemple — voici à quoi ressemble un direct actif"
|
||||||
demo_meta: "PalaTigers · Match Live TV"
|
demo_meta: "Pala MLTV · Match Live TV"
|
||||||
demo_sets: "Set 2 · Sets gagnés 1-0"
|
demo_sets: "Set 2 · Sets gagnés 1-0"
|
||||||
show:
|
show:
|
||||||
back_to_all: "← Tous les directs"
|
back_to_all: "← Tous les directs"
|
||||||
@@ -805,7 +808,7 @@ fr:
|
|||||||
back_to_live: "← Directs en cours"
|
back_to_live: "← Directs en cours"
|
||||||
title: "Directs passés"
|
title: "Directs passés"
|
||||||
hint: "Replays publics des clubs sportifs — concerne les matchs déjà diffusés."
|
hint: "Replays publics des clubs sportifs — concerne les matchs déjà diffusés."
|
||||||
search_placeholder: "Ex. Tigers Volley, adversaire, club, lieu…"
|
search_placeholder: "Ex. Team MLTV, Team Guest…"
|
||||||
search_aria_label: "Rechercher un replay"
|
search_aria_label: "Rechercher un replay"
|
||||||
search_button: "Rechercher"
|
search_button: "Rechercher"
|
||||||
reset_link: "Réinitialiser"
|
reset_link: "Réinitialiser"
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
it:
|
it:
|
||||||
|
api:
|
||||||
|
errors:
|
||||||
|
user_concurrent_stream: "Hai già una diretta in corso con questo account. Chiudila prima di avviarne un’altra."
|
||||||
password_policy:
|
password_policy:
|
||||||
hint: "Minimo 8 caratteri, con almeno 3 tra: minuscole, maiuscole, numeri e simboli."
|
hint: "Minimo 8 caratteri, con almeno 3 tra: minuscole, maiuscole, numeri e simboli."
|
||||||
activerecord:
|
activerecord:
|
||||||
@@ -82,10 +85,10 @@ it:
|
|||||||
title: "Registra società — Match Live TV"
|
title: "Registra società — Match Live TV"
|
||||||
meta_description: Crea la società sportiva e la prima squadra su Match Live TV.
|
meta_description: Crea la società sportiva e la prima squadra su Match Live TV.
|
||||||
heading: La tua società
|
heading: La tua società
|
||||||
lead: "Registra il club: potrai aggiungere più squadre (Under 13, Under 15, Serie C…)."
|
lead: "Registra il club: potrai aggiungere più squadre (Under 13, Under 15, prima squadra…)."
|
||||||
section_club: Società
|
section_club: Società
|
||||||
name_label: Nome società / club
|
name_label: Nome società / club
|
||||||
name_placeholder: "es. Crazy Volley Rozzano"
|
name_placeholder: "es. Team MLTV"
|
||||||
section_first_team: Prima squadra
|
section_first_team: Prima squadra
|
||||||
default_first_team_name: Prima squadra
|
default_first_team_name: Prima squadra
|
||||||
first_team_name_label: Nome squadra
|
first_team_name_label: Nome squadra
|
||||||
@@ -164,7 +167,7 @@ it:
|
|||||||
heading: Nuova squadra
|
heading: Nuova squadra
|
||||||
club_label: "Società:"
|
club_label: "Società:"
|
||||||
name_label: Nome squadra
|
name_label: Nome squadra
|
||||||
name_placeholder: "es. Under 13, Serie C"
|
name_placeholder: "es. U15 maschile"
|
||||||
branding_legend: Override branding (opzionale)
|
branding_legend: Override branding (opzionale)
|
||||||
submit: Aggiungi squadra
|
submit: Aggiungi squadra
|
||||||
invite:
|
invite:
|
||||||
@@ -240,7 +243,7 @@ it:
|
|||||||
matches:
|
matches:
|
||||||
back_to_list: "← Elenco partite"
|
back_to_list: "← Elenco partite"
|
||||||
opponent_label: Avversario
|
opponent_label: Avversario
|
||||||
opponent_placeholder: "es. Volley Milano"
|
opponent_placeholder: "es. Squadra ospite"
|
||||||
location_label: "Luogo (opzionale)"
|
location_label: "Luogo (opzionale)"
|
||||||
location_placeholder: "Palestra, città"
|
location_placeholder: "Palestra, città"
|
||||||
datetime_label: Data e ora
|
datetime_label: Data e ora
|
||||||
@@ -367,7 +370,7 @@ it:
|
|||||||
legend_hint: "Tutti i campi contrassegnati sono obbligatori per abbonarti a un piano premium e per emettere le fatture. Per le società serve la P.IVA; per le persone fisiche il Codice Fiscale. Serve SDI oppure PEC. I pagamenti restano gestiti in modo sicuro da Stripe."
|
legend_hint: "Tutti i campi contrassegnati sono obbligatori per abbonarti a un piano premium e per emettere le fatture. Per le società serve la P.IVA; per le persone fisiche il Codice Fiscale. Serve SDI oppure PEC. I pagamenti restano gestiti in modo sicuro da Stripe."
|
||||||
entity_type_label: "Tipo intestatario *"
|
entity_type_label: "Tipo intestatario *"
|
||||||
legal_name_label: "Ragione sociale / nome e cognome *"
|
legal_name_label: "Ragione sociale / nome e cognome *"
|
||||||
legal_name_placeholder: "es. ASD Tigers Volley"
|
legal_name_placeholder: "es. Team MLTV"
|
||||||
vat_number_label: "Partita IVA * (società)"
|
vat_number_label: "Partita IVA * (società)"
|
||||||
fiscal_code_label: "Codice Fiscale * (persona fisica)"
|
fiscal_code_label: "Codice Fiscale * (persona fisica)"
|
||||||
fiscal_code_placeholder: RSSMRA80A01H501U
|
fiscal_code_placeholder: RSSMRA80A01H501U
|
||||||
@@ -771,7 +774,7 @@ it:
|
|||||||
replay_archive_link: "Live passate — archivio replay"
|
replay_archive_link: "Live passate — archivio replay"
|
||||||
schedule_match_link: "Programma partita"
|
schedule_match_link: "Programma partita"
|
||||||
search_placeholder_club: "Cerca società, squadra, avversario o luogo…"
|
search_placeholder_club: "Cerca società, squadra, avversario o luogo…"
|
||||||
search_placeholder_default: "Es. Crazy Volley, Serie D, avversario…"
|
search_placeholder_default: "Es. Team MLTV, Squadra ospite…"
|
||||||
search_aria_label: "Cerca squadra"
|
search_aria_label: "Cerca squadra"
|
||||||
search_button: "Cerca"
|
search_button: "Cerca"
|
||||||
reset_link: "Azzera"
|
reset_link: "Azzera"
|
||||||
@@ -805,7 +808,7 @@ it:
|
|||||||
empty_hero_cta_features: "Scopri come funziona"
|
empty_hero_cta_features: "Scopri come funziona"
|
||||||
demo_aria_label: "Esempio di diretta attiva"
|
demo_aria_label: "Esempio di diretta attiva"
|
||||||
demo_label: "Esempio — così appare una diretta attiva"
|
demo_label: "Esempio — così appare una diretta attiva"
|
||||||
demo_meta: "PalaTigers · Match Live TV"
|
demo_meta: "Pala MLTV · Match Live TV"
|
||||||
demo_sets: "Set 2 · Set vinti 1-0"
|
demo_sets: "Set 2 · Set vinti 1-0"
|
||||||
show:
|
show:
|
||||||
back_to_all: "← Tutte le dirette"
|
back_to_all: "← Tutte le dirette"
|
||||||
@@ -838,7 +841,7 @@ it:
|
|||||||
back_to_live: "← Dirette live"
|
back_to_live: "← Dirette live"
|
||||||
title: "Live passate"
|
title: "Live passate"
|
||||||
hint: "Replay pubblici delle società sportive — riguarda le partite già trasmesse."
|
hint: "Replay pubblici delle società sportive — riguarda le partite già trasmesse."
|
||||||
search_placeholder: "Es. Tigers Volley, avversario, società, luogo…"
|
search_placeholder: "Es. Team MLTV, Squadra ospite…"
|
||||||
search_aria_label: "Cerca replay"
|
search_aria_label: "Cerca replay"
|
||||||
search_button: "Cerca"
|
search_button: "Cerca"
|
||||||
reset_link: "Azzera"
|
reset_link: "Azzera"
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
it:
|
||||||
|
demo:
|
||||||
|
away_team: Squadra ospite
|
||||||
|
when: "Sabato 12 settembre · 10:30"
|
||||||
|
en:
|
||||||
|
demo:
|
||||||
|
away_team: Team Guest
|
||||||
|
when: "Saturday 12 September · 10:30"
|
||||||
|
de:
|
||||||
|
demo:
|
||||||
|
away_team: Team Guest
|
||||||
|
when: "Samstag, 12. September · 10:30"
|
||||||
|
fr:
|
||||||
|
demo:
|
||||||
|
away_team: Team Guest
|
||||||
|
when: "Samedi 12 septembre · 10:30"
|
||||||
|
es:
|
||||||
|
demo:
|
||||||
|
away_team: Team Guest
|
||||||
|
when: "Sábado 12 de septiembre · 10:30"
|
||||||
@@ -59,11 +59,8 @@ de:
|
|||||||
replay_item_download: Video bei Bedarf aufs Handy herunterladen
|
replay_item_download: Video bei Bedarf aufs Handy herunterladen
|
||||||
replay_card_title: Spielarchiv
|
replay_card_title: Spielarchiv
|
||||||
replay_card_body: Spiele nach dem Schlusspfiff noch einmal ansehen und bei Bedarf herunterladen.
|
replay_card_body: Spiele nach dem Schlusspfiff noch einmal ansehen und bei Bedarf herunterladen.
|
||||||
replay_mock_1_title: Rossi vs Neri
|
|
||||||
replay_mock_1_meta: "12/05 · 01:25:34"
|
replay_mock_1_meta: "12/05 · 01:25:34"
|
||||||
replay_mock_2_title: Rossi vs Blu
|
|
||||||
replay_mock_2_meta: "05/05 · 01:18:22"
|
replay_mock_2_meta: "05/05 · 01:18:22"
|
||||||
replay_mock_3_title: Rossi vs Bianchi
|
|
||||||
replay_mock_3_meta: "28/04 · 01:07:15"
|
replay_mock_3_meta: "28/04 · 01:07:15"
|
||||||
more_title: Auch das gehört zum Produkt
|
more_title: Auch das gehört zum Produkt
|
||||||
more_stable_title: Livestream vom Smartphone
|
more_stable_title: Livestream vom Smartphone
|
||||||
@@ -76,6 +73,19 @@ de:
|
|||||||
cta_body: Probieren Sie Match Live TV kostenlos. Keine spezielle Ausrüstung nötig.
|
cta_body: Probieren Sie Match Live TV kostenlos. Keine spezielle Ausrüstung nötig.
|
||||||
cta_primary: Kostenlos starten
|
cta_primary: Kostenlos starten
|
||||||
cta_secondary: Pläne vergleichen
|
cta_secondary: Pläne vergleichen
|
||||||
|
sponsor_cover:
|
||||||
|
eyebrow: Premium Full
|
||||||
|
title: Gib deinen Sponsoren Sichtbarkeit
|
||||||
|
body: Mit Premium Full kannst du das Titelbild jedes Spiels anpassen, indem du eine Grafik eures Vereins hochlädst. Darin könnt ihr Sponsoren, Partner, Logos und Botschaften unterbringen und ihnen schon vor dem Livestream Sichtbarkeit geben.
|
||||||
|
item_cover: Titelbild für jedes Spiel hochladen
|
||||||
|
item_before: Grafik sichtbar im Pre-Live
|
||||||
|
item_club: Die Identität eures Vereins, kein generisches Template
|
||||||
|
claim: Euer Verein. Eure Sponsoren. Euer Livestream.
|
||||||
|
cta: Premium Full entdecken
|
||||||
|
mock:
|
||||||
|
state: PRE-LIVE
|
||||||
|
art_label: Individuelles Titelbild
|
||||||
|
soon: Der Livestream beginnt in Kürze
|
||||||
pricing:
|
pricing:
|
||||||
meta_title: "Preise für Jugendspiel-Streaming — Match Live TV"
|
meta_title: "Preise für Jugendspiel-Streaming — Match Live TV"
|
||||||
meta_description: "Free-, Premium-Light- und Premium-Full-Pläne für Livestreams und Spielarchiv. Jahresabo für Vereine: mehr Team-Mitglieder, mehr parallele Spiele, Replay und YouTube."
|
meta_description: "Free-, Premium-Light- und Premium-Full-Pläne für Livestreams und Spielarchiv. Jahresabo für Vereine: mehr Team-Mitglieder, mehr parallele Spiele, Replay und YouTube."
|
||||||
@@ -90,6 +100,7 @@ de:
|
|||||||
table_youtube: YouTube
|
table_youtube: YouTube
|
||||||
table_replay: Replay-Archiv
|
table_replay: Replay-Archiv
|
||||||
table_download: Handy-Download
|
table_download: Handy-Download
|
||||||
|
table_cover_sponsor: Individuelles Titelbild
|
||||||
table_price: Preis
|
table_price: Preis
|
||||||
table_price_free: "€0"
|
table_price_free: "€0"
|
||||||
table_price_note: "Listenpreis %{list} — %{monthly}"
|
table_price_note: "Listenpreis %{list} — %{monthly}"
|
||||||
@@ -121,6 +132,7 @@ de:
|
|||||||
youtube_mltv: Match Live TV
|
youtube_mltv: Match Live TV
|
||||||
youtube_club: Vereinskanal
|
youtube_club: Vereinskanal
|
||||||
youtube_none: nein
|
youtube_none: nein
|
||||||
|
cover_sponsor_html: "Anpassbares Titelbild <strong>mit Sponsoren</strong>"
|
||||||
complete_billing: Rechnungsdaten vervollständigen
|
complete_billing: Rechnungsdaten vervollständigen
|
||||||
start_free: Kostenlos starten
|
start_free: Kostenlos starten
|
||||||
register_with_price: "Registrieren — %{price}"
|
register_with_price: "Registrieren — %{price}"
|
||||||
@@ -150,6 +162,8 @@ de:
|
|||||||
q9_question: Wie funktioniert die Garantie „Zufrieden oder Geld zurück“?
|
q9_question: Wie funktioniert die Garantie „Zufrieden oder Geld zurück“?
|
||||||
q9_answer_html: "Sie können Match Live TV 30 Tage ab Aktivierung des ersten Abos mit Ihrem Verein testen. Wenn die Plattform in diesem Zeitraum nicht zu Ihren Anforderungen passt, können Sie die Erstattung des vollen gezahlten Betrags beantragen. Weitere Details finden Sie in den %{terms_link}."
|
q9_answer_html: "Sie können Match Live TV 30 Tage ab Aktivierung des ersten Abos mit Ihrem Verein testen. Wenn die Plattform in diesem Zeitraum nicht zu Ihren Anforderungen passt, können Sie die Erstattung des vollen gezahlten Betrags beantragen. Weitere Details finden Sie in den %{terms_link}."
|
||||||
q9_terms_link: Nutzungsbedingungen
|
q9_terms_link: Nutzungsbedingungen
|
||||||
|
q10_question: Kann ich die Sponsoren des Vereins in den Übertragungen zeigen?
|
||||||
|
q10_answer: Mit Premium Full kannst du ein individuelles Titelbild für das Spiel hochladen. Der Verein bereitet die eigene Grafik vor und kann darin Sponsoren, Partner, Logos und Botschaften unterbringen. Das Titelbild erscheint vor Beginn des Livestreams und gibt den Unterstützern des Clubs Sichtbarkeit.
|
||||||
cta_signup: Team registrieren
|
cta_signup: Team registrieren
|
||||||
cta_live: Live-Spiele ansehen
|
cta_live: Live-Spiele ansehen
|
||||||
volleyball:
|
volleyball:
|
||||||
|
|||||||
@@ -59,11 +59,8 @@ en:
|
|||||||
replay_item_download: Download the video to your phone when you need it
|
replay_item_download: Download the video to your phone when you need it
|
||||||
replay_card_title: Match archive
|
replay_card_title: Match archive
|
||||||
replay_card_body: Rewatch matches after the final whistle and download them when you need them.
|
replay_card_body: Rewatch matches after the final whistle and download them when you need them.
|
||||||
replay_mock_1_title: Rossi vs Neri
|
|
||||||
replay_mock_1_meta: "12/05 · 01:25:34"
|
replay_mock_1_meta: "12/05 · 01:25:34"
|
||||||
replay_mock_2_title: Rossi vs Blu
|
|
||||||
replay_mock_2_meta: "05/05 · 01:18:22"
|
replay_mock_2_meta: "05/05 · 01:18:22"
|
||||||
replay_mock_3_title: Rossi vs Bianchi
|
|
||||||
replay_mock_3_meta: "28/04 · 01:07:15"
|
replay_mock_3_meta: "28/04 · 01:07:15"
|
||||||
more_title: Also part of the product
|
more_title: Also part of the product
|
||||||
more_stable_title: Stream from your smartphone
|
more_stable_title: Stream from your smartphone
|
||||||
@@ -76,6 +73,19 @@ en:
|
|||||||
cta_body: Try Match Live TV for free. No dedicated equipment needed.
|
cta_body: Try Match Live TV for free. No dedicated equipment needed.
|
||||||
cta_primary: Start for free
|
cta_primary: Start for free
|
||||||
cta_secondary: Compare plans
|
cta_secondary: Compare plans
|
||||||
|
sponsor_cover:
|
||||||
|
eyebrow: Premium Full
|
||||||
|
title: Give your sponsors visibility
|
||||||
|
body: With Premium Full you can customise each match cover by uploading artwork from your club. You can include sponsors, partners, logos and messages in that graphic, giving them visibility before the live stream starts.
|
||||||
|
item_cover: A cover uploaded for every match
|
||||||
|
item_before: Artwork shown in the pre-live screen
|
||||||
|
item_club: Your club’s identity, not a generic template
|
||||||
|
claim: Your club. Your sponsors. Your stream.
|
||||||
|
cta: Discover Premium Full
|
||||||
|
mock:
|
||||||
|
state: PRE-LIVE
|
||||||
|
art_label: Custom cover
|
||||||
|
soon: The live stream will start shortly
|
||||||
pricing:
|
pricing:
|
||||||
meta_title: "Youth match streaming pricing — Match Live TV"
|
meta_title: "Youth match streaming pricing — Match Live TV"
|
||||||
meta_description: "Free, Premium Light and Premium Full plans for live streaming and match archive. Annual subscription for clubs: more staff, more concurrent matches, replay and YouTube."
|
meta_description: "Free, Premium Light and Premium Full plans for live streaming and match archive. Annual subscription for clubs: more staff, more concurrent matches, replay and YouTube."
|
||||||
@@ -90,6 +100,7 @@ en:
|
|||||||
table_youtube: YouTube
|
table_youtube: YouTube
|
||||||
table_replay: Replay archive
|
table_replay: Replay archive
|
||||||
table_download: Phone download
|
table_download: Phone download
|
||||||
|
table_cover_sponsor: Custom cover
|
||||||
table_price: Price
|
table_price: Price
|
||||||
table_price_free: "€0"
|
table_price_free: "€0"
|
||||||
table_price_note: "list %{list} — %{monthly}"
|
table_price_note: "list %{list} — %{monthly}"
|
||||||
@@ -121,6 +132,7 @@ en:
|
|||||||
youtube_mltv: Match Live TV
|
youtube_mltv: Match Live TV
|
||||||
youtube_club: club channel
|
youtube_club: club channel
|
||||||
youtube_none: "no"
|
youtube_none: "no"
|
||||||
|
cover_sponsor_html: "Customisable cover <strong>with sponsors</strong>"
|
||||||
complete_billing: Complete billing details
|
complete_billing: Complete billing details
|
||||||
start_free: Start for free
|
start_free: Start for free
|
||||||
register_with_price: "Register — %{price}"
|
register_with_price: "Register — %{price}"
|
||||||
@@ -150,6 +162,8 @@ en:
|
|||||||
q9_question: How does the Satisfied or refunded guarantee work?
|
q9_question: How does the Satisfied or refunded guarantee work?
|
||||||
q9_answer_html: "You can try Match Live TV with your club for 30 days from activation of the first subscription. If within that period you feel the platform is not a good fit, you can request a refund of the full amount paid. For more details see the %{terms_link}."
|
q9_answer_html: "You can try Match Live TV with your club for 30 days from activation of the first subscription. If within that period you feel the platform is not a good fit, you can request a refund of the full amount paid. For more details see the %{terms_link}."
|
||||||
q9_terms_link: Terms and Conditions
|
q9_terms_link: Terms and Conditions
|
||||||
|
q10_question: Can I include the club’s sponsors in the broadcasts?
|
||||||
|
q10_answer: With Premium Full you can upload a custom cover for the match. The club prepares its own artwork and can include sponsors, partners, logos and messages in that graphic. The cover is shown before the live stream starts, giving visibility to the organisations that support the club.
|
||||||
cta_signup: Register your team
|
cta_signup: Register your team
|
||||||
cta_live: Watch live matches
|
cta_live: Watch live matches
|
||||||
volleyball:
|
volleyball:
|
||||||
|
|||||||
@@ -59,11 +59,8 @@ es:
|
|||||||
replay_item_download: Descarga del vídeo al móvil cuando haga falta
|
replay_item_download: Descarga del vídeo al móvil cuando haga falta
|
||||||
replay_card_title: Archivo de partidos
|
replay_card_title: Archivo de partidos
|
||||||
replay_card_body: Vuelve a ver los partidos tras el pitido final y descárgalos cuando los necesites.
|
replay_card_body: Vuelve a ver los partidos tras el pitido final y descárgalos cuando los necesites.
|
||||||
replay_mock_1_title: Rossi vs Neri
|
|
||||||
replay_mock_1_meta: "12/05 · 01:25:34"
|
replay_mock_1_meta: "12/05 · 01:25:34"
|
||||||
replay_mock_2_title: Rossi vs Blu
|
|
||||||
replay_mock_2_meta: "05/05 · 01:18:22"
|
replay_mock_2_meta: "05/05 · 01:18:22"
|
||||||
replay_mock_3_title: Rossi vs Bianchi
|
|
||||||
replay_mock_3_meta: "28/04 · 01:07:15"
|
replay_mock_3_meta: "28/04 · 01:07:15"
|
||||||
more_title: También forma parte del producto
|
more_title: También forma parte del producto
|
||||||
more_stable_title: Directo desde el smartphone
|
more_stable_title: Directo desde el smartphone
|
||||||
@@ -76,6 +73,19 @@ es:
|
|||||||
cta_body: Prueba Match Live TV gratis. No hace falta equipo dedicado.
|
cta_body: Prueba Match Live TV gratis. No hace falta equipo dedicado.
|
||||||
cta_primary: Empieza gratis
|
cta_primary: Empieza gratis
|
||||||
cta_secondary: Compara los planes
|
cta_secondary: Compara los planes
|
||||||
|
sponsor_cover:
|
||||||
|
eyebrow: Premium Full
|
||||||
|
title: Da visibilidad a tus patrocinadores
|
||||||
|
body: Con Premium Full puedes personalizar la portada de cada partido subiendo una gráfica de tu club. Así puedes incluir patrocinadores, partners, logotipos y mensajes, y darles visibilidad ya antes de que empiece el directo.
|
||||||
|
item_cover: Portada cargada para cada partido
|
||||||
|
item_before: Gráfica visible en el pre-directo
|
||||||
|
item_club: La identidad de tu club, no una plantilla genérica
|
||||||
|
claim: Tu club. Tus patrocinadores. Tu directo.
|
||||||
|
cta: Descubre Premium Full
|
||||||
|
mock:
|
||||||
|
state: PRE-LIVE
|
||||||
|
art_label: Portada personalizada
|
||||||
|
soon: El directo comenzará en breve
|
||||||
pricing:
|
pricing:
|
||||||
meta_title: "Precios de streaming de partidos juveniles — Match Live TV"
|
meta_title: "Precios de streaming de partidos juveniles — Match Live TV"
|
||||||
meta_description: "Planes Free, Premium Light y Premium Full para directos y archivo de partidos. Suscripción anual para clubes: más staff, más partidos en paralelo, repetición y YouTube."
|
meta_description: "Planes Free, Premium Light y Premium Full para directos y archivo de partidos. Suscripción anual para clubes: más staff, más partidos en paralelo, repetición y YouTube."
|
||||||
@@ -90,6 +100,7 @@ es:
|
|||||||
table_youtube: YouTube
|
table_youtube: YouTube
|
||||||
table_replay: Archivo de replay
|
table_replay: Archivo de replay
|
||||||
table_download: Descarga al móvil
|
table_download: Descarga al móvil
|
||||||
|
table_cover_sponsor: Portada personalizada
|
||||||
table_price: Precio
|
table_price: Precio
|
||||||
table_price_free: "€0"
|
table_price_free: "€0"
|
||||||
table_price_note: "tarifa %{list} — %{monthly}"
|
table_price_note: "tarifa %{list} — %{monthly}"
|
||||||
@@ -121,6 +132,7 @@ es:
|
|||||||
youtube_mltv: Match Live TV
|
youtube_mltv: Match Live TV
|
||||||
youtube_club: canal del club
|
youtube_club: canal del club
|
||||||
youtube_none: "no"
|
youtube_none: "no"
|
||||||
|
cover_sponsor_html: "Portada personalizable <strong>con patrocinadores</strong>"
|
||||||
complete_billing: Completar datos de facturación
|
complete_billing: Completar datos de facturación
|
||||||
start_free: Empieza gratis
|
start_free: Empieza gratis
|
||||||
register_with_price: "Regístrate — %{price}"
|
register_with_price: "Regístrate — %{price}"
|
||||||
@@ -150,6 +162,8 @@ es:
|
|||||||
q9_question: "¿Cómo funciona la garantía Satisfechos o reembolsados?"
|
q9_question: "¿Cómo funciona la garantía Satisfechos o reembolsados?"
|
||||||
q9_answer_html: "Puedes probar Match Live TV con tu club durante 30 días desde la activación de la primera suscripción. Si en ese periodo consideras que la plataforma no se adapta a tus necesidades, puedes solicitar el reembolso del importe íntegro pagado. Para más detalles consulta los %{terms_link}."
|
q9_answer_html: "Puedes probar Match Live TV con tu club durante 30 días desde la activación de la primera suscripción. Si en ese periodo consideras que la plataforma no se adapta a tus necesidades, puedes solicitar el reembolso del importe íntegro pagado. Para más detalles consulta los %{terms_link}."
|
||||||
q9_terms_link: Términos y Condiciones
|
q9_terms_link: Términos y Condiciones
|
||||||
|
q10_question: ¿Puedo incluir a los patrocinadores del club en los directos?
|
||||||
|
q10_answer: Con Premium Full puedes subir una portada personalizada para el partido. El club prepara su propia gráfica e incluye patrocinadores, partners, logotipos y mensajes. La portada se muestra antes del inicio del directo, dando visibilidad a quienes apoyan al club.
|
||||||
cta_signup: Registra tu equipo
|
cta_signup: Registra tu equipo
|
||||||
cta_live: Ver directos
|
cta_live: Ver directos
|
||||||
volleyball:
|
volleyball:
|
||||||
|
|||||||
@@ -59,11 +59,8 @@ fr:
|
|||||||
replay_item_download: Téléchargement de la vidéo sur le téléphone si besoin
|
replay_item_download: Téléchargement de la vidéo sur le téléphone si besoin
|
||||||
replay_card_title: Archive des matchs
|
replay_card_title: Archive des matchs
|
||||||
replay_card_body: Revoyez les matchs après le coup de sifflet final et téléchargez-les quand vous en avez besoin.
|
replay_card_body: Revoyez les matchs après le coup de sifflet final et téléchargez-les quand vous en avez besoin.
|
||||||
replay_mock_1_title: Rossi vs Neri
|
|
||||||
replay_mock_1_meta: "12/05 · 01:25:34"
|
replay_mock_1_meta: "12/05 · 01:25:34"
|
||||||
replay_mock_2_title: Rossi vs Blu
|
|
||||||
replay_mock_2_meta: "05/05 · 01:18:22"
|
replay_mock_2_meta: "05/05 · 01:18:22"
|
||||||
replay_mock_3_title: Rossi vs Bianchi
|
|
||||||
replay_mock_3_meta: "28/04 · 01:07:15"
|
replay_mock_3_meta: "28/04 · 01:07:15"
|
||||||
more_title: Cela fait aussi partie du produit
|
more_title: Cela fait aussi partie du produit
|
||||||
more_stable_title: Direct depuis le smartphone
|
more_stable_title: Direct depuis le smartphone
|
||||||
@@ -76,6 +73,19 @@ fr:
|
|||||||
cta_body: Essayez Match Live TV gratuitement. Aucun matériel dédié nécessaire.
|
cta_body: Essayez Match Live TV gratuitement. Aucun matériel dédié nécessaire.
|
||||||
cta_primary: Commencer gratuitement
|
cta_primary: Commencer gratuitement
|
||||||
cta_secondary: Comparer les offres
|
cta_secondary: Comparer les offres
|
||||||
|
sponsor_cover:
|
||||||
|
eyebrow: Premium Full
|
||||||
|
title: Donnez de la visibilité à vos sponsors
|
||||||
|
body: Avec Premium Full, vous pouvez personnaliser la jaquette de chaque match en chargeant un graphisme de votre club. Vous pouvez y intégrer sponsors, partenaires, logos et messages, et leur donner de la visibilité avant le début du direct.
|
||||||
|
item_cover: Jaquette chargée pour chaque match
|
||||||
|
item_before: Graphisme visible en pré-direct
|
||||||
|
item_club: L’identité de votre club, pas un modèle générique
|
||||||
|
claim: Votre club. Vos sponsors. Votre direct.
|
||||||
|
cta: Découvrir Premium Full
|
||||||
|
mock:
|
||||||
|
state: PRE-LIVE
|
||||||
|
art_label: Jaquette personnalisée
|
||||||
|
soon: Le direct va bientôt commencer
|
||||||
pricing:
|
pricing:
|
||||||
meta_title: "Tarifs streaming des matchs jeunes — Match Live TV"
|
meta_title: "Tarifs streaming des matchs jeunes — Match Live TV"
|
||||||
meta_description: "Offres Free, Premium Light et Premium Full pour le direct et l'archive des matchs. Abonnement annuel pour les clubs : plus de staff, plus de matchs en parallèle, replay et YouTube."
|
meta_description: "Offres Free, Premium Light et Premium Full pour le direct et l'archive des matchs. Abonnement annuel pour les clubs : plus de staff, plus de matchs en parallèle, replay et YouTube."
|
||||||
@@ -90,6 +100,7 @@ fr:
|
|||||||
table_youtube: YouTube
|
table_youtube: YouTube
|
||||||
table_replay: Archive replay
|
table_replay: Archive replay
|
||||||
table_download: Téléchargement téléphone
|
table_download: Téléchargement téléphone
|
||||||
|
table_cover_sponsor: Jaquette personnalisée
|
||||||
table_price: Prix
|
table_price: Prix
|
||||||
table_price_free: "€0"
|
table_price_free: "€0"
|
||||||
table_price_note: "tarif %{list} — %{monthly}"
|
table_price_note: "tarif %{list} — %{monthly}"
|
||||||
@@ -121,6 +132,7 @@ fr:
|
|||||||
youtube_mltv: Match Live TV
|
youtube_mltv: Match Live TV
|
||||||
youtube_club: chaîne du club
|
youtube_club: chaîne du club
|
||||||
youtube_none: non
|
youtube_none: non
|
||||||
|
cover_sponsor_html: "Jaquette personnalisable <strong>avec sponsors</strong>"
|
||||||
complete_billing: Compléter les données de facturation
|
complete_billing: Compléter les données de facturation
|
||||||
start_free: Commencer gratuitement
|
start_free: Commencer gratuitement
|
||||||
register_with_price: "S'inscrire — %{price}"
|
register_with_price: "S'inscrire — %{price}"
|
||||||
@@ -150,6 +162,8 @@ fr:
|
|||||||
q9_question: Comment fonctionne la garantie Satisfait ou remboursé ?
|
q9_question: Comment fonctionne la garantie Satisfait ou remboursé ?
|
||||||
q9_answer_html: "Vous pouvez essayer Match Live TV avec votre club pendant 30 jours à compter de l'activation du premier abonnement. Si dans ce délai la plateforme ne convient pas à vos besoins, vous pouvez demander le remboursement de la totalité du montant payé. Pour plus de détails, consultez les %{terms_link}."
|
q9_answer_html: "Vous pouvez essayer Match Live TV avec votre club pendant 30 jours à compter de l'activation du premier abonnement. Si dans ce délai la plateforme ne convient pas à vos besoins, vous pouvez demander le remboursement de la totalité du montant payé. Pour plus de détails, consultez les %{terms_link}."
|
||||||
q9_terms_link: Conditions générales
|
q9_terms_link: Conditions générales
|
||||||
|
q10_question: Puis-je afficher les sponsors du club dans les directs ?
|
||||||
|
q10_answer: Avec Premium Full, vous pouvez charger une jaquette personnalisée pour le match. Le club prépare son propre graphisme et peut y intégrer sponsors, partenaires, logos et messages. La jaquette s’affiche avant le début du direct, donnant de la visibilité aux réalités qui soutiennent le club.
|
||||||
cta_signup: Inscrire l'équipe
|
cta_signup: Inscrire l'équipe
|
||||||
cta_live: Voir les directs
|
cta_live: Voir les directs
|
||||||
volleyball:
|
volleyball:
|
||||||
|
|||||||
@@ -59,11 +59,8 @@ it:
|
|||||||
replay_item_download: Download del video sul telefono quando serve
|
replay_item_download: Download del video sul telefono quando serve
|
||||||
replay_card_title: Archivio partite
|
replay_card_title: Archivio partite
|
||||||
replay_card_body: Rivedi le gare dopo il fischio finale e scaricale quando ti servono.
|
replay_card_body: Rivedi le gare dopo il fischio finale e scaricale quando ti servono.
|
||||||
replay_mock_1_title: Rossi vs Neri
|
|
||||||
replay_mock_1_meta: "12/05 · 01:25:34"
|
replay_mock_1_meta: "12/05 · 01:25:34"
|
||||||
replay_mock_2_title: Rossi vs Blu
|
|
||||||
replay_mock_2_meta: "05/05 · 01:18:22"
|
replay_mock_2_meta: "05/05 · 01:18:22"
|
||||||
replay_mock_3_title: Rossi vs Bianchi
|
|
||||||
replay_mock_3_meta: "28/04 · 01:07:15"
|
replay_mock_3_meta: "28/04 · 01:07:15"
|
||||||
more_title: Anche questo fa parte del prodotto
|
more_title: Anche questo fa parte del prodotto
|
||||||
more_stable_title: Diretta dallo smartphone
|
more_stable_title: Diretta dallo smartphone
|
||||||
@@ -76,6 +73,19 @@ it:
|
|||||||
cta_body: Prova Match Live TV gratuitamente. Non serve attrezzatura dedicata.
|
cta_body: Prova Match Live TV gratuitamente. Non serve attrezzatura dedicata.
|
||||||
cta_primary: Inizia gratis
|
cta_primary: Inizia gratis
|
||||||
cta_secondary: Confronta i piani
|
cta_secondary: Confronta i piani
|
||||||
|
sponsor_cover:
|
||||||
|
eyebrow: Premium Full
|
||||||
|
title: Dai visibilità ai tuoi sponsor
|
||||||
|
body: Con Premium Full puoi personalizzare la copertina di ogni partita caricando una grafica della tua società. Puoi così inserire sponsor, partner, loghi e comunicazioni e dare loro visibilità già prima dell’inizio della diretta.
|
||||||
|
item_cover: Copertina caricata per ogni partita
|
||||||
|
item_before: Grafica visibile nel pre-live
|
||||||
|
item_club: Identità della società, non un template generico
|
||||||
|
claim: Il tuo club. I tuoi sponsor. La tua diretta.
|
||||||
|
cta: Scopri Premium Full
|
||||||
|
mock:
|
||||||
|
state: PRE-LIVE
|
||||||
|
art_label: Copertina personalizzata
|
||||||
|
soon: La diretta inizierà a breve
|
||||||
pricing:
|
pricing:
|
||||||
meta_title: "Prezzi streaming partite giovanili — Match Live TV"
|
meta_title: "Prezzi streaming partite giovanili — Match Live TV"
|
||||||
meta_description: "Piani Free, Premium Light e Premium Full per dirette live e archivio partite. Abbonamento annuale per società: più staff, più partite in parallelo, replay e YouTube."
|
meta_description: "Piani Free, Premium Light e Premium Full per dirette live e archivio partite. Abbonamento annuale per società: più staff, più partite in parallelo, replay e YouTube."
|
||||||
@@ -90,6 +100,7 @@ it:
|
|||||||
table_youtube: YouTube
|
table_youtube: YouTube
|
||||||
table_replay: Archivio replay
|
table_replay: Archivio replay
|
||||||
table_download: Download telefono
|
table_download: Download telefono
|
||||||
|
table_cover_sponsor: Copertina personalizzata
|
||||||
table_price: Prezzo
|
table_price: Prezzo
|
||||||
table_price_free: "€0"
|
table_price_free: "€0"
|
||||||
table_price_note: "listino %{list} — %{monthly}"
|
table_price_note: "listino %{list} — %{monthly}"
|
||||||
@@ -121,6 +132,7 @@ it:
|
|||||||
youtube_mltv: Match Live TV
|
youtube_mltv: Match Live TV
|
||||||
youtube_club: canale società
|
youtube_club: canale società
|
||||||
youtube_none: "no"
|
youtube_none: "no"
|
||||||
|
cover_sponsor_html: "Copertina personalizzabile <strong>con sponsor</strong>"
|
||||||
complete_billing: Completa dati di fatturazione
|
complete_billing: Completa dati di fatturazione
|
||||||
start_free: Inizia gratis
|
start_free: Inizia gratis
|
||||||
register_with_price: "Registrati — %{price}"
|
register_with_price: "Registrati — %{price}"
|
||||||
@@ -150,6 +162,8 @@ it:
|
|||||||
q9_question: Come funziona la garanzia Soddisfatti o rimborsati?
|
q9_question: Come funziona la garanzia Soddisfatti o rimborsati?
|
||||||
q9_answer_html: "Puoi provare Match Live TV con la tua società per 30 giorni dall'attivazione del primo abbonamento. Se entro questo periodo ritieni che la piattaforma non sia adatta alle tue esigenze, puoi richiedere il rimborso dell'intero importo pagato. Per maggiori dettagli consulta i %{terms_link}."
|
q9_answer_html: "Puoi provare Match Live TV con la tua società per 30 giorni dall'attivazione del primo abbonamento. Se entro questo periodo ritieni che la piattaforma non sia adatta alle tue esigenze, puoi richiedere il rimborso dell'intero importo pagato. Per maggiori dettagli consulta i %{terms_link}."
|
||||||
q9_terms_link: Termini e Condizioni
|
q9_terms_link: Termini e Condizioni
|
||||||
|
q10_question: Posso inserire gli sponsor della società nelle dirette?
|
||||||
|
q10_answer: Con Premium Full puoi caricare una copertina personalizzata per la partita. La società può preparare la propria grafica inserendo sponsor, partner, loghi e comunicazioni. La copertina viene mostrata prima dell’inizio della diretta, permettendo di dare visibilità alle realtà che sostengono il club.
|
||||||
cta_signup: Registra la squadra
|
cta_signup: Registra la squadra
|
||||||
cta_live: Guarda le dirette
|
cta_live: Guarda le dirette
|
||||||
volleyball:
|
volleyball:
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ Rails.application.routes.draw do
|
|||||||
post :regia_link
|
post :regia_link
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
resources :stream_concurrency_violations, only: %i[index]
|
||||||
resources :stream_nodes, only: %i[index create destroy] do
|
resources :stream_nodes, only: %i[index create destroy] do
|
||||||
member do
|
member do
|
||||||
post :drain
|
post :drain
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateStreamConcurrencyViolations < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
create_table :stream_concurrency_violations, id: :uuid, default: -> { "gen_random_uuid()" } do |t|
|
||||||
|
t.references :user, type: :uuid, foreign_key: { on_delete: :nullify }
|
||||||
|
t.references :occupying_session, type: :uuid, foreign_key: { to_table: :stream_sessions, on_delete: :nullify }
|
||||||
|
t.references :attempted_session, type: :uuid, foreign_key: { to_table: :stream_sessions, on_delete: :nullify }
|
||||||
|
t.references :occupying_club, type: :uuid, foreign_key: { to_table: :clubs, on_delete: :nullify }
|
||||||
|
t.references :attempted_club, type: :uuid, foreign_key: { to_table: :clubs, on_delete: :nullify }
|
||||||
|
|
||||||
|
t.string :attempt_action, null: false, default: "start"
|
||||||
|
t.string :user_email, null: false
|
||||||
|
t.string :user_name
|
||||||
|
t.string :occupying_club_name
|
||||||
|
t.string :attempted_club_name
|
||||||
|
t.string :occupying_match_label, null: false
|
||||||
|
t.string :attempted_match_label, null: false
|
||||||
|
t.string :occupying_status
|
||||||
|
t.string :occupying_device
|
||||||
|
t.string :attempted_device
|
||||||
|
t.boolean :devices_differ, null: false, default: false
|
||||||
|
t.jsonb :metadata, null: false, default: {}
|
||||||
|
|
||||||
|
t.datetime :created_at, null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :stream_concurrency_violations, :created_at
|
||||||
|
add_index :stream_concurrency_violations, :devices_differ
|
||||||
|
add_index :stream_sessions, %i[user_id status], name: "index_stream_sessions_on_user_id_and_status"
|
||||||
|
end
|
||||||
|
end
|
||||||
Generated
+36
-2
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.2].define(version: 2026_08_28_180000) do
|
ActiveRecord::Schema[7.2].define(version: 2026_08_31_193000) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pgcrypto"
|
enable_extension "pgcrypto"
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
@@ -383,6 +383,34 @@ ActiveRecord::Schema[7.2].define(version: 2026_08_28_180000) do
|
|||||||
t.index ["stream_session_id"], name: "index_score_states_on_stream_session_id", unique: true
|
t.index ["stream_session_id"], name: "index_score_states_on_stream_session_id", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "stream_concurrency_violations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||||
|
t.uuid "user_id"
|
||||||
|
t.uuid "occupying_session_id"
|
||||||
|
t.uuid "attempted_session_id"
|
||||||
|
t.uuid "occupying_club_id"
|
||||||
|
t.uuid "attempted_club_id"
|
||||||
|
t.string "attempt_action", default: "start", null: false
|
||||||
|
t.string "user_email", null: false
|
||||||
|
t.string "user_name"
|
||||||
|
t.string "occupying_club_name"
|
||||||
|
t.string "attempted_club_name"
|
||||||
|
t.string "occupying_match_label", null: false
|
||||||
|
t.string "attempted_match_label", null: false
|
||||||
|
t.string "occupying_status"
|
||||||
|
t.string "occupying_device"
|
||||||
|
t.string "attempted_device"
|
||||||
|
t.boolean "devices_differ", default: false, null: false
|
||||||
|
t.jsonb "metadata", default: {}, null: false
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.index ["attempted_club_id"], name: "index_stream_concurrency_violations_on_attempted_club_id"
|
||||||
|
t.index ["attempted_session_id"], name: "index_stream_concurrency_violations_on_attempted_session_id"
|
||||||
|
t.index ["created_at"], name: "index_stream_concurrency_violations_on_created_at"
|
||||||
|
t.index ["devices_differ"], name: "index_stream_concurrency_violations_on_devices_differ"
|
||||||
|
t.index ["occupying_club_id"], name: "index_stream_concurrency_violations_on_occupying_club_id"
|
||||||
|
t.index ["occupying_session_id"], name: "index_stream_concurrency_violations_on_occupying_session_id"
|
||||||
|
t.index ["user_id"], name: "index_stream_concurrency_violations_on_user_id"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "stream_events", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
create_table "stream_events", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||||
t.uuid "stream_session_id", null: false
|
t.uuid "stream_session_id", null: false
|
||||||
t.string "event_type", null: false
|
t.string "event_type", null: false
|
||||||
@@ -440,9 +468,9 @@ ActiveRecord::Schema[7.2].define(version: 2026_08_28_180000) do
|
|||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.string "regia_token_digest"
|
t.string "regia_token_digest"
|
||||||
t.datetime "regia_token_expires_at"
|
t.datetime "regia_token_expires_at"
|
||||||
|
t.boolean "audio_muted", default: false, null: false
|
||||||
t.uuid "stream_node_id"
|
t.uuid "stream_node_id"
|
||||||
t.string "min_quality_preset", default: "auto", null: false
|
t.string "min_quality_preset", default: "auto", null: false
|
||||||
t.boolean "audio_muted", default: false, null: false
|
|
||||||
t.string "client_os"
|
t.string "client_os"
|
||||||
t.string "app_version"
|
t.string "app_version"
|
||||||
t.string "app_build"
|
t.string "app_build"
|
||||||
@@ -458,6 +486,7 @@ ActiveRecord::Schema[7.2].define(version: 2026_08_28_180000) do
|
|||||||
t.index ["regia_token_digest"], name: "index_stream_sessions_on_regia_token_digest", unique: true
|
t.index ["regia_token_digest"], name: "index_stream_sessions_on_regia_token_digest", unique: true
|
||||||
t.index ["status"], name: "index_stream_sessions_on_status"
|
t.index ["status"], name: "index_stream_sessions_on_status"
|
||||||
t.index ["stream_node_id"], name: "index_stream_sessions_on_stream_node_id"
|
t.index ["stream_node_id"], name: "index_stream_sessions_on_stream_node_id"
|
||||||
|
t.index ["user_id", "status"], name: "index_stream_sessions_on_user_id_and_status"
|
||||||
t.index ["user_id"], name: "index_stream_sessions_on_user_id"
|
t.index ["user_id"], name: "index_stream_sessions_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -591,6 +620,11 @@ ActiveRecord::Schema[7.2].define(version: 2026_08_28_180000) do
|
|||||||
add_foreign_key "recordings", "stream_sessions"
|
add_foreign_key "recordings", "stream_sessions"
|
||||||
add_foreign_key "recordings", "teams"
|
add_foreign_key "recordings", "teams"
|
||||||
add_foreign_key "score_states", "stream_sessions"
|
add_foreign_key "score_states", "stream_sessions"
|
||||||
|
add_foreign_key "stream_concurrency_violations", "clubs", column: "attempted_club_id", on_delete: :nullify
|
||||||
|
add_foreign_key "stream_concurrency_violations", "clubs", column: "occupying_club_id", on_delete: :nullify
|
||||||
|
add_foreign_key "stream_concurrency_violations", "stream_sessions", column: "attempted_session_id", on_delete: :nullify
|
||||||
|
add_foreign_key "stream_concurrency_violations", "stream_sessions", column: "occupying_session_id", on_delete: :nullify
|
||||||
|
add_foreign_key "stream_concurrency_violations", "users", on_delete: :nullify
|
||||||
add_foreign_key "stream_events", "stream_sessions"
|
add_foreign_key "stream_events", "stream_sessions"
|
||||||
add_foreign_key "stream_sessions", "matches"
|
add_foreign_key "stream_sessions", "matches"
|
||||||
add_foreign_key "stream_sessions", "stream_nodes"
|
add_foreign_key "stream_sessions", "stream_nodes"
|
||||||
|
|||||||
+5
-5
@@ -16,7 +16,7 @@ admin = User.find_or_create_by!(email: "admin@matchlivetv.test") do |u|
|
|||||||
u.role = "admin"
|
u.role = "admin"
|
||||||
end
|
end
|
||||||
|
|
||||||
club = Club.find_or_create_by!(name: "Tigers Volley") do |c|
|
club = Club.find_or_create_by!(name: MatchLiveTv::Demo.home_team) do |c|
|
||||||
c.sport = "volleyball"
|
c.sport = "volleyball"
|
||||||
c.primary_color = "#e53935"
|
c.primary_color = "#e53935"
|
||||||
c.secondary_color = "#ffffff"
|
c.secondary_color = "#ffffff"
|
||||||
@@ -24,7 +24,7 @@ end
|
|||||||
|
|
||||||
ClubMembership.find_or_create_by!(user: coach, club: club) { |m| m.role = "owner" }
|
ClubMembership.find_or_create_by!(user: coach, club: club) { |m| m.role = "owner" }
|
||||||
|
|
||||||
team = club.teams.find_or_create_by!(name: "Under 16") do |t|
|
team = club.teams.find_or_create_by!(name: MatchLiveTv::Demo::CATEGORY) do |t|
|
||||||
t.sport = "volleyball"
|
t.sport = "volleyball"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -32,12 +32,12 @@ UserTeam.find_or_create_by!(user: admin, team: team) { |ut| ut.role = "member" }
|
|||||||
|
|
||||||
Billing::AssignPlan.call(club: club, plan_slug: "free") unless club.subscription
|
Billing::AssignPlan.call(club: club, plan_slug: "free") unless club.subscription
|
||||||
|
|
||||||
match = team.matches.find_or_create_by!(opponent_name: "ASD Eagles Pavia") do |m|
|
match = team.matches.find_or_create_by!(opponent_name: MatchLiveTv::Demo::AWAY_TEAM) do |m|
|
||||||
m.location = "PalaTigers - Milano"
|
m.location = MatchLiveTv::Demo.venue
|
||||||
m.scheduled_at = 2.hours.from_now
|
m.scheduled_at = 2.hours.from_now
|
||||||
m.sets_to_win = 3
|
m.sets_to_win = 3
|
||||||
m.roster_numbers = [4, 6, 8, 9, 10, 11, 12, 14, 16, 17, 21]
|
m.roster_numbers = [4, 6, 8, 9, 10, 11, 12, 14, 16, 17, 21]
|
||||||
m.category = "U16"
|
m.category = MatchLiveTv::Demo::CATEGORY
|
||||||
m.phase = "Semifinale"
|
m.phase = "Semifinale"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ coach = User.find_or_create_by!(email: "coach@matchlivetv.test") do |u|
|
|||||||
end
|
end
|
||||||
|
|
||||||
clubs_data = [
|
clubs_data = [
|
||||||
{ name: "Tigers Volley", sport: "volleyball", plan: "premium_full", sessions: 14, hours: 18.5, revenue_yearly: 19900 },
|
{ name: MatchLiveTv::Demo::HOME_TEAM, 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: "Team MLTV Nord", 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: "Team MLTV Sud", 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: "Team MLTV Basket", 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 }
|
{ name: MatchLiveTv::Demo::AWAY_TEAM, sport: "volleyball", plan: "free", sessions: 2, hours: 2.0, revenue_yearly: 0 }
|
||||||
]
|
]
|
||||||
|
|
||||||
sport_keys = {
|
sport_keys = {
|
||||||
@@ -81,7 +81,7 @@ clubs.each_with_index do |row, index|
|
|||||||
started = day.change(hour: 10 + (n % 6), min: 0)
|
started = day.change(hour: 10 + (n % 6), min: 0)
|
||||||
ended = started + secs.seconds
|
ended = started + secs.seconds
|
||||||
match = team.matches.create!(
|
match = team.matches.create!(
|
||||||
opponent_name: "Avversario demo #{n + 1}",
|
opponent_name: MatchLiveTv::Demo::AWAY_TEAM,
|
||||||
sport_key: team.sport_key,
|
sport_key: team.sport_key,
|
||||||
scheduled_at: started
|
scheduled_at: started
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module MatchLiveTv
|
||||||
|
# Set demo ufficiale per mockup, preview e contenuti dimostrativi del sito.
|
||||||
|
# Nomi fittizi, chiaramente riconducibili a MatchLiveTV — mai società o sponsor reali.
|
||||||
|
module Demo
|
||||||
|
HOME_TEAM = "Team MLTV"
|
||||||
|
AWAY_TEAM = "Team Guest"
|
||||||
|
CATEGORY = "U15 MASCHILE"
|
||||||
|
VENUE = "Pala MLTV"
|
||||||
|
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def home_team
|
||||||
|
HOME_TEAM
|
||||||
|
end
|
||||||
|
|
||||||
|
def away_team
|
||||||
|
I18n.t("demo.away_team", default: AWAY_TEAM)
|
||||||
|
end
|
||||||
|
|
||||||
|
def category
|
||||||
|
CATEGORY
|
||||||
|
end
|
||||||
|
|
||||||
|
def when_label
|
||||||
|
I18n.t("demo.when")
|
||||||
|
end
|
||||||
|
|
||||||
|
def venue
|
||||||
|
VENUE
|
||||||
|
end
|
||||||
|
|
||||||
|
def match_title
|
||||||
|
"#{home_team} vs #{away_team}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def live_meta
|
||||||
|
"#{VENUE} · Match Live TV"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -236,6 +236,11 @@ body.admin-body {
|
|||||||
.badge--ingest-cloud { background: #e65100; color: #fff3e0; }
|
.badge--ingest-cloud { background: #e65100; color: #fff3e0; }
|
||||||
.badge--ended { background: #37474f; color: #eceff1; }
|
.badge--ended { background: #37474f; color: #eceff1; }
|
||||||
.badge--error { background: #b71c1c; color: #ffebee; }
|
.badge--error { background: #b71c1c; color: #ffebee; }
|
||||||
|
.badge--abuse { background: #b71c1c; color: #ffebee; }
|
||||||
|
|
||||||
|
.admin-table tr.admin-row--two-devices td {
|
||||||
|
background: rgba(183, 28, 28, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
.admin-ingest {
|
.admin-ingest {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|||||||
@@ -3713,6 +3713,273 @@ a.replay-archive__thumb:hover {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.feature-card__badge--gold {
|
||||||
|
background: rgba(255, 183, 77, 0.12);
|
||||||
|
color: #ffb74d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsor-cover { padding-top: 8px; }
|
||||||
|
.sponsor-cover.section { padding-top: 28px; padding-bottom: 28px; }
|
||||||
|
.sponsor-cover--compact.section { padding-top: 16px; padding-bottom: 8px; }
|
||||||
|
.sponsor-cover__panel {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.95fr);
|
||||||
|
gap: 32px;
|
||||||
|
align-items: center;
|
||||||
|
background: #14141c;
|
||||||
|
border: 1px solid #5a2a2e;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 32px 28px;
|
||||||
|
box-shadow: 0 0 0 1px rgba(229, 57, 53, 0.18), 0 12px 32px rgba(0, 0, 0, 0.28);
|
||||||
|
}
|
||||||
|
.sponsor-cover--compact .sponsor-cover__panel {
|
||||||
|
padding: 24px 22px;
|
||||||
|
gap: 24px;
|
||||||
|
box-shadow: 0 0 0 1px rgba(229, 57, 53, 0.12);
|
||||||
|
}
|
||||||
|
.sponsor-cover__copy { min-width: 0; }
|
||||||
|
.sponsor-cover__copy .feature-card__badge { margin-bottom: 12px; }
|
||||||
|
.sponsor-cover__copy h2 {
|
||||||
|
text-align: left;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 1.7rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
.sponsor-cover--compact .sponsor-cover__copy h2 { font-size: 1.4rem; }
|
||||||
|
.sponsor-cover__lead {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
color: #bbb;
|
||||||
|
line-height: 1.6;
|
||||||
|
max-width: 36rem;
|
||||||
|
}
|
||||||
|
.sponsor-cover--compact .sponsor-cover__lead { margin-bottom: 18px; }
|
||||||
|
.sponsor-cover__copy .features-checklist { margin-bottom: 14px; }
|
||||||
|
.sponsor-cover__claim {
|
||||||
|
margin: 0 0 18px;
|
||||||
|
color: #ffb74d;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.sponsor-cover__visual {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.cover-mock {
|
||||||
|
width: min(100%, 380px);
|
||||||
|
margin: 0;
|
||||||
|
background: #101016;
|
||||||
|
border: 1px solid #2f2f3c;
|
||||||
|
border-radius: 14px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
.sponsor-cover--compact .cover-mock { width: min(100%, 320px); }
|
||||||
|
.cover-mock__chrome {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-bottom: 1px solid #2a2a36;
|
||||||
|
background: #16161e;
|
||||||
|
}
|
||||||
|
.cover-mock__brand {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.cover-mock__brand em {
|
||||||
|
font-style: normal;
|
||||||
|
color: #e53935;
|
||||||
|
}
|
||||||
|
.cover-mock__state {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(255, 183, 77, 0.14);
|
||||||
|
color: #ffb74d;
|
||||||
|
font-size: 0.62rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
.cover-mock__stage {
|
||||||
|
padding: 16px 16px 14px;
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse 80% 50% at 50% 0%, rgba(229, 57, 53, 0.16), transparent 60%),
|
||||||
|
#101016;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.cover-mock__cat {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
color: #9a9aaa;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.cover-mock__home,
|
||||||
|
.cover-mock__away {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.sponsor-cover--compact .cover-mock__home,
|
||||||
|
.sponsor-cover--compact .cover-mock__away { font-size: 0.92rem; }
|
||||||
|
.cover-mock__vs {
|
||||||
|
margin: 4px 0;
|
||||||
|
color: #e53935;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.cover-mock__when {
|
||||||
|
margin: 8px 0 10px;
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
.cover-mock__art-label {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
color: #8e8e9a;
|
||||||
|
font-size: 0.62rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.cover-mock__art {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 168px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 22px 16px 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background:
|
||||||
|
linear-gradient(155deg, rgba(229, 57, 53, 0.22) 0%, transparent 38%),
|
||||||
|
linear-gradient(180deg, #1c1418 0%, #121218 55%, #0e0e14 100%);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
.cover-mock__art::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: -24%;
|
||||||
|
right: -18%;
|
||||||
|
width: 58%;
|
||||||
|
height: 90%;
|
||||||
|
background: linear-gradient(135deg, rgba(229, 57, 53, 0.28), transparent 70%);
|
||||||
|
transform: rotate(18deg);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.sponsor-cover--compact .cover-mock__art {
|
||||||
|
min-height: 132px;
|
||||||
|
padding: 16px 14px 14px;
|
||||||
|
}
|
||||||
|
.cover-mock__crest {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.cover-mock__crest-mark {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: linear-gradient(180deg, #e53935, #b71c1c);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
box-shadow: 0 8px 18px rgba(229, 57, 53, 0.28);
|
||||||
|
}
|
||||||
|
.sponsor-cover--compact .cover-mock__crest-mark {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
font-size: 1rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.cover-mock__crest-name {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.cover-mock__marks {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
.cover-mock__mark {
|
||||||
|
display: block;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 4px;
|
||||||
|
opacity: 0.72;
|
||||||
|
}
|
||||||
|
.cover-mock__mark--a {
|
||||||
|
width: 46px;
|
||||||
|
background: linear-gradient(90deg, #c9a227, #f0d78c);
|
||||||
|
}
|
||||||
|
.cover-mock__mark--b {
|
||||||
|
width: 34px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #d7d7e0;
|
||||||
|
}
|
||||||
|
.cover-mock__mark--c {
|
||||||
|
width: 40px;
|
||||||
|
background: #8aa4c8;
|
||||||
|
}
|
||||||
|
.cover-mock__soon {
|
||||||
|
margin: 0;
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 999px) {
|
||||||
|
.sponsor-cover__panel {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
text-align: center;
|
||||||
|
padding: 24px 20px;
|
||||||
|
}
|
||||||
|
.sponsor-cover__copy .feature-card__badge { align-self: center; }
|
||||||
|
.sponsor-cover__copy {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.sponsor-cover__copy h2 { text-align: center; }
|
||||||
|
.sponsor-cover__lead { margin-left: auto; margin-right: auto; }
|
||||||
|
.sponsor-cover__copy .features-checklist { text-align: left; width: 100%; max-width: 28rem; }
|
||||||
|
.cover-mock { width: min(100%, 360px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.sponsor-cover__panel { padding: 20px 16px; }
|
||||||
|
.sponsor-cover__copy h2 { font-size: 1.35rem; }
|
||||||
|
.cover-mock__home,
|
||||||
|
.cover-mock__away { font-size: 0.95rem; }
|
||||||
|
.cover-mock__art { min-height: 148px; }
|
||||||
|
}
|
||||||
|
|
||||||
.mltv-preview-badge {
|
.mltv-preview-badge {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 16px;
|
bottom: 16px;
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "rails_helper"
|
||||||
|
|
||||||
|
RSpec.describe MatchLiveTv::Demo do
|
||||||
|
it "usa Team MLTV come squadra demo ufficiale" do
|
||||||
|
expect(described_class.home_team).to eq("Team MLTV")
|
||||||
|
expect(described_class.category).to eq("U15 MASCHILE")
|
||||||
|
expect(described_class.venue).to eq("Pala MLTV")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "localizza l'avversario senza nomi di società reali" do
|
||||||
|
I18n.with_locale(:it) do
|
||||||
|
expect(described_class.away_team).to eq("Squadra ospite")
|
||||||
|
expect(described_class.match_title).to eq("Team MLTV vs Squadra ospite")
|
||||||
|
end
|
||||||
|
I18n.with_locale(:en) do
|
||||||
|
expect(described_class.away_team).to eq("Team Guest")
|
||||||
|
expect(described_class.match_title).to eq("Team MLTV vs Team Guest")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "rails_helper"
|
||||||
|
|
||||||
|
RSpec.describe "Admin stream concurrency violations", type: :request do
|
||||||
|
let!(:admin) { AdminAccount.create!(username: "ops-abuse", password: "Password123") }
|
||||||
|
let!(:user) { User.create!(email: "abuse@test.it", name: "Abuser", password: "Password123", role: "coach") }
|
||||||
|
let!(:club) { Club.create!(name: "Abuse Club", sport: "volleyball") }
|
||||||
|
let!(:team) { club.teams.create!(name: "U16", sport: "volleyball") }
|
||||||
|
let!(:match_a) { team.matches.create!(opponent_name: "Rival A", sport: "volleyball") }
|
||||||
|
let!(:match_b) { team.matches.create!(opponent_name: "Rival B", sport: "volleyball") }
|
||||||
|
let!(:live) do
|
||||||
|
StreamSession.create!(match: match_a, user: user, platform: "matchlivetv", status: "live")
|
||||||
|
end
|
||||||
|
let!(:idle) do
|
||||||
|
StreamSession.create!(match: match_b, user: user, platform: "matchlivetv", status: "idle")
|
||||||
|
end
|
||||||
|
let!(:violation) do
|
||||||
|
StreamConcurrencyViolation.record!(attempted: idle, occupying: live, action: "start")
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
post admin_login_path, params: { username: admin.username, password: "Password123" }
|
||||||
|
end
|
||||||
|
|
||||||
|
it "mostra l'elenco abusi" do
|
||||||
|
get admin_stream_concurrency_violations_path
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(response.body).to include("abuse@test.it")
|
||||||
|
expect(response.body).to include("Abuse Club")
|
||||||
|
expect(response.body).to include("Rival A")
|
||||||
|
expect(response.body).to include("Rival B")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "mostra il badge in dashboard" do
|
||||||
|
get admin_root_path
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(response.body).to include("abuse@test.it")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "mostra i tentativi nella scheda società" do
|
||||||
|
get admin_club_path(club)
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(response.body).to include("abuse@test.it")
|
||||||
|
expect(response.body).to include("Rival B")
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "rails_helper"
|
||||||
|
|
||||||
|
RSpec.describe "API start seconda diretta stesso account", type: :request do
|
||||||
|
let!(:user) { User.create!(email: "conc@test.it", name: "C", password: "Password123", role: "coach") }
|
||||||
|
let!(:club) { Club.create!(name: "Conc Club", sport: "volleyball") }
|
||||||
|
let!(:membership) { club.club_memberships.create!(user: user, role: "owner") }
|
||||||
|
let!(:team) { club.teams.create!(name: "Tigers", sport: "volleyball") }
|
||||||
|
let!(:match_a) { team.matches.create!(opponent_name: "A", sport: "volleyball") }
|
||||||
|
let!(:match_b) { team.matches.create!(opponent_name: "B", sport: "volleyball") }
|
||||||
|
let!(:live) do
|
||||||
|
StreamSession.create!(
|
||||||
|
match: match_a,
|
||||||
|
user: user,
|
||||||
|
platform: "matchlivetv",
|
||||||
|
status: "live",
|
||||||
|
started_at: 5.minutes.ago,
|
||||||
|
device_model: "Pixel 8",
|
||||||
|
client_os: "android"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
let!(:idle) do
|
||||||
|
StreamSession.create!(
|
||||||
|
match: match_b,
|
||||||
|
user: user,
|
||||||
|
platform: "matchlivetv",
|
||||||
|
status: "idle",
|
||||||
|
device_model: "iPhone 15",
|
||||||
|
client_os: "ios"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def auth_headers
|
||||||
|
post "/api/v1/auth/login", params: { email: user.email, password: "Password123" }
|
||||||
|
token = response.parsed_body["access_token"]
|
||||||
|
{ "Authorization" => "Bearer #{token}", "Content-Type" => "application/json" }
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
plan = Plan.find_or_initialize_by(slug: "premium_full")
|
||||||
|
plan.name ||= "Premium Full"
|
||||||
|
plan.features = (plan.features || {}).merge(
|
||||||
|
"platforms" => %w[matchlivetv],
|
||||||
|
"concurrent_streams_limit" => 10
|
||||||
|
)
|
||||||
|
plan.save!
|
||||||
|
club.create_subscription!(plan: plan, status: "active") if club.subscription.blank?
|
||||||
|
allow(SessionChannel).to receive(:broadcast_message)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "restituisce 403 user_concurrent_stream e salva l'evidenza" do
|
||||||
|
patch "/api/v1/sessions/#{idle.id}/start", headers: auth_headers
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
body = response.parsed_body
|
||||||
|
expect(body["error_code"]).to eq("user_concurrent_stream")
|
||||||
|
expect(body["error"]).to be_present
|
||||||
|
expect(StreamConcurrencyViolation.where(attempted_session_id: idle.id, occupying_session_id: live.id)).to exist
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -16,7 +16,17 @@ RSpec.describe "Public features page", type: :request do
|
|||||||
expect(response.body).to include("Chi filma, filma")
|
expect(response.body).to include("Chi filma, filma")
|
||||||
expect(response.body).to include("Accesso con link")
|
expect(response.body).to include("Accesso con link")
|
||||||
expect(response.body).to include("La diretta resta della tua società")
|
expect(response.body).to include("La diretta resta della tua società")
|
||||||
expect(response.body).to include("La partita finisce. Il ricordo resta.")
|
expect(response.body).to include("Dai visibilità ai tuoi sponsor")
|
||||||
|
expect(response.body).to include("Copertina caricata per ogni partita")
|
||||||
|
expect(response.body).to include("Scopri Premium Full")
|
||||||
|
expect(response.body).to include("Team MLTV")
|
||||||
|
expect(response.body).to include("Squadra ospite")
|
||||||
|
expect(response.body).to include("Copertina personalizzata")
|
||||||
|
expect(response.body).not_to include("Lupi Santa Croce")
|
||||||
|
expect(response.body).not_to include("Rossi vs Neri")
|
||||||
|
expect(response.body).not_to include("Tigers Volley")
|
||||||
|
expect(response.body).not_to include("MAIN SPONSOR")
|
||||||
|
expect(response.body).not_to include("CON IL SUPPORTO DI")
|
||||||
expect(response.body).to include("Inizia gratis")
|
expect(response.body).to include("Inizia gratis")
|
||||||
expect(response.body).to include("Confronta i piani")
|
expect(response.body).to include("Confronta i piani")
|
||||||
expect(response.body).to include(public_signup_path)
|
expect(response.body).to include(public_signup_path)
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ RSpec.describe "Public home page", type: :request do
|
|||||||
expect(response.body).to include(I18n.t("home.stores_label", locale: :it))
|
expect(response.body).to include(I18n.t("home.stores_label", locale: :it))
|
||||||
expect(response.body).to include(I18n.t("home.store_app_store_name", locale: :it))
|
expect(response.body).to include(I18n.t("home.store_app_store_name", locale: :it))
|
||||||
expect(response.body).to include(I18n.t("home.store_play_name", locale: :it))
|
expect(response.body).to include(I18n.t("home.store_play_name", locale: :it))
|
||||||
|
expect(response.body).to include("Dai visibilità ai tuoi sponsor")
|
||||||
|
expect(response.body).to include("Team MLTV")
|
||||||
|
expect(response.body).not_to include("Lupi Santa Croce")
|
||||||
|
expect(response.body).to include("Scopri Premium Full")
|
||||||
expect(response.body.scan(MatchLiveTv.app_store_url).size).to eq(3)
|
expect(response.body.scan(MatchLiveTv.app_store_url).size).to eq(3)
|
||||||
expect(response.body.scan(MatchLiveTv.play_store_url).size).to eq(3)
|
expect(response.body.scan(MatchLiveTv.play_store_url).size).to eq(3)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -26,6 +26,14 @@ RSpec.describe "Public pricing page", type: :request do
|
|||||||
expect(response.body).to include("Account trasmettitori")
|
expect(response.body).to include("Account trasmettitori")
|
||||||
expect(response.body).to include("Niente password condivise")
|
expect(response.body).to include("Niente password condivise")
|
||||||
expect(response.body).to include("plan-card--featured")
|
expect(response.body).to include("plan-card--featured")
|
||||||
|
expect(response.body).to include("Copertina personalizzabile")
|
||||||
|
expect(response.body).to include("con sponsor")
|
||||||
|
expect(response.body).to include("Copertina personalizzata")
|
||||||
|
expect(response.body).to include("Dai visibilità ai tuoi sponsor")
|
||||||
|
expect(response.body).to include("Team MLTV")
|
||||||
|
expect(response.body).not_to include("Lupi Santa Croce")
|
||||||
|
expect(response.body).not_to include("MAIN SPONSOR")
|
||||||
|
expect(response.body).not_to include("CON IL SUPPORTO DI")
|
||||||
expect(response.body).to include("Soddisfatti o rimborsati")
|
expect(response.body).to include("Soddisfatti o rimborsati")
|
||||||
expect(response.body).to include("30 giorni soddisfatti o rimborsati")
|
expect(response.body).to include("30 giorni soddisfatti o rimborsati")
|
||||||
expect(response.body).to include("ti rimborsiamo il 100%")
|
expect(response.body).to include("ti rimborsiamo il 100%")
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
require "rails_helper"
|
require "rails_helper"
|
||||||
|
|
||||||
RSpec.describe "Public team page", type: :request do
|
RSpec.describe "Public team page", type: :request do
|
||||||
let(:club) { Club.create!(name: "ASD Eagles", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff") }
|
let(:club) { Club.create!(name: "Team MLTV", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff") }
|
||||||
let!(:team) do
|
let!(:team) do
|
||||||
club.teams.create!(
|
club.teams.create!(
|
||||||
name: "Tigers Volley U17",
|
name: "Team MLTV U15",
|
||||||
sport_key: "pallavolo",
|
sport_key: "pallavolo",
|
||||||
description: "Squadra giovanile under 17.",
|
description: "Squadra giovanile under 15.",
|
||||||
roster_public: true
|
roster_public: true
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@@ -15,9 +15,9 @@ RSpec.describe "Public team page", type: :request do
|
|||||||
get public_team_page_path(team.slug)
|
get public_team_page_path(team.slug)
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
expect(response.body).to include("Tigers Volley U17")
|
expect(response.body).to include("Team MLTV U15")
|
||||||
expect(response.body).to include("ASD Eagles")
|
expect(response.body).to include("Team MLTV")
|
||||||
expect(response.body).to include("Squadra giovanile under 17")
|
expect(response.body).to include("Squadra giovanile under 15")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "restituisce 404 per slug inesistente" do
|
it "restituisce 404 per slug inesistente" do
|
||||||
@@ -41,13 +41,13 @@ RSpec.describe "Public team page", type: :request do
|
|||||||
end
|
end
|
||||||
|
|
||||||
RSpec.describe "Public team directory", type: :request do
|
RSpec.describe "Public team directory", type: :request do
|
||||||
let(:club) { Club.create!(name: "ASD Eagles", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff") }
|
let(:club) { Club.create!(name: "Team MLTV", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff") }
|
||||||
let(:user) { User.create!(email: "dir2@test.com", name: "Coach", password: "Password123", role: "coach") }
|
let(:user) { User.create!(email: "dir2@test.com", name: "Coach", password: "Password123", role: "coach") }
|
||||||
let!(:team) { club.teams.create!(name: "Tigers Volley U17", sport_key: "pallavolo") }
|
let!(:team) { club.teams.create!(name: "Team MLTV U15", sport_key: "pallavolo") }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
club.club_memberships.create!(user: user, role: "owner")
|
club.club_memberships.create!(user: user, role: "owner")
|
||||||
match = team.matches.create!(opponent_name: "Rival", sport_key: "pallavolo", scheduled_at: 1.day.from_now)
|
match = team.matches.create!(opponent_name: MatchLiveTv::Demo::AWAY_TEAM, sport_key: "pallavolo", scheduled_at: 1.day.from_now)
|
||||||
StreamSession.create!(
|
StreamSession.create!(
|
||||||
match: match, user: user, status: "live", platform: "matchlivetv",
|
match: match, user: user, status: "live", platform: "matchlivetv",
|
||||||
privacy_status: "public", publish_token: "tok-dir", started_at: Time.current
|
privacy_status: "public", publish_token: "tok-dir", started_at: Time.current
|
||||||
@@ -58,13 +58,13 @@ RSpec.describe "Public team directory", type: :request do
|
|||||||
get public_team_pages_path
|
get public_team_pages_path
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
expect(response.body).to include("Tigers Volley U17")
|
expect(response.body).to include("Team MLTV U15")
|
||||||
expect(response.body).to include("In diretta")
|
expect(response.body).to include("In diretta")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "filtra per sport via query string" do
|
it "filtra per sport via query string" do
|
||||||
get public_team_pages_path(sport: "basket")
|
get public_team_pages_path(sport: "basket")
|
||||||
|
|
||||||
expect(response.body).not_to include("Tigers Volley U17")
|
expect(response.body).not_to include("Team MLTV U15")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "rails_helper"
|
||||||
|
|
||||||
|
RSpec.describe Sessions::Start, "one live per user" do
|
||||||
|
let(:user) { User.create!(email: "tx@test.it", name: "Tx", password: "Password123", role: "coach") }
|
||||||
|
let(:club) { Club.create!(name: "Club A", sport: "volleyball") }
|
||||||
|
let(:team) { club.teams.create!(name: "Team A", sport: "volleyball") }
|
||||||
|
let(:match_a) { team.matches.create!(opponent_name: "Opp A", sport: "volleyball") }
|
||||||
|
let(:match_b) { team.matches.create!(opponent_name: "Opp B", sport: "volleyball") }
|
||||||
|
|
||||||
|
before do
|
||||||
|
club.club_memberships.create!(user: user, role: "owner")
|
||||||
|
plan = Plan.find_or_initialize_by(slug: "premium_full")
|
||||||
|
plan.name ||= "Premium Full"
|
||||||
|
plan.features = (plan.features || {}).merge(
|
||||||
|
"platforms" => %w[matchlivetv youtube],
|
||||||
|
"concurrent_streams_limit" => 10
|
||||||
|
)
|
||||||
|
plan.save!
|
||||||
|
club.create_subscription!(plan: plan, status: "active") if club.subscription.blank?
|
||||||
|
allow(SessionChannel).to receive(:broadcast_message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def session_for(match, status:, device_model: "Pixel 8")
|
||||||
|
StreamSession.create!(
|
||||||
|
match: match,
|
||||||
|
user: user,
|
||||||
|
platform: "matchlivetv",
|
||||||
|
status: status,
|
||||||
|
device_manufacturer: "Google",
|
||||||
|
device_model: device_model,
|
||||||
|
client_os: "android"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "avvia la prima diretta" do
|
||||||
|
session = session_for(match_a, status: "idle")
|
||||||
|
described_class.new(session).call
|
||||||
|
expect(session.reload.status).to eq("connecting")
|
||||||
|
expect(StreamConcurrencyViolation.count).to eq(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "blocca la seconda diretta dello stesso account e registra l'abuso" do
|
||||||
|
live = session_for(match_a, status: "live", device_model: "Pixel 8")
|
||||||
|
live.update!(started_at: 10.minutes.ago)
|
||||||
|
second = session_for(match_b, status: "idle", device_model: "iPhone 15")
|
||||||
|
second.update!(client_os: "ios", device_manufacturer: "Apple")
|
||||||
|
|
||||||
|
expect {
|
||||||
|
described_class.new(second).call
|
||||||
|
}.to raise_error(Teams::EntitlementError) { |e|
|
||||||
|
expect(e.code).to eq("user_concurrent_stream")
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(second.reload.status).to eq("idle")
|
||||||
|
violation = StreamConcurrencyViolation.last
|
||||||
|
expect(violation).to be_present
|
||||||
|
expect(violation.user_email).to eq(user.email)
|
||||||
|
expect(violation.occupying_session_id).to eq(live.id)
|
||||||
|
expect(violation.attempted_session_id).to eq(second.id)
|
||||||
|
expect(violation.devices_differ).to be(true)
|
||||||
|
expect(violation.attempt_action).to eq("start")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "non blocca un secondo account sulla stessa società" do
|
||||||
|
other = User.create!(email: "tx2@test.it", name: "Tx2", password: "Password123", role: "coach")
|
||||||
|
club.club_memberships.create!(user: other, role: "owner")
|
||||||
|
session_for(match_a, status: "live")
|
||||||
|
other_session = StreamSession.create!(
|
||||||
|
match: match_b,
|
||||||
|
user: other,
|
||||||
|
platform: "matchlivetv",
|
||||||
|
status: "idle"
|
||||||
|
)
|
||||||
|
|
||||||
|
described_class.new(other_session).call
|
||||||
|
expect(other_session.reload.status).to eq("connecting")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "considera occupante anche una sessione in pausa" do
|
||||||
|
session_for(match_a, status: "paused")
|
||||||
|
second = session_for(match_b, status: "idle")
|
||||||
|
|
||||||
|
expect {
|
||||||
|
described_class.new(second).call
|
||||||
|
}.to raise_error(Teams::EntitlementError) { |e|
|
||||||
|
expect(e.code).to eq("user_concurrent_stream")
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it "non considera occupante una sessione idle" do
|
||||||
|
session_for(match_a, status: "idle")
|
||||||
|
second = session_for(match_b, status: "idle")
|
||||||
|
|
||||||
|
described_class.new(second).call
|
||||||
|
expect(second.reload.status).to eq("connecting")
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -4,13 +4,13 @@ RSpec.describe Teams::GenerateSlug do
|
|||||||
let(:club) { Club.create!(name: "Test Club", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff") }
|
let(:club) { Club.create!(name: "Test Club", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff") }
|
||||||
|
|
||||||
it "genera uno slug dal nome" do
|
it "genera uno slug dal nome" do
|
||||||
team = club.teams.build(name: "Crazy Volley U17B", sport_key: "pallavolo")
|
team = club.teams.build(name: "Team MLTV U15", sport_key: "pallavolo")
|
||||||
expect(described_class.call(team)).to eq("crazy-volley-u17b")
|
expect(described_class.call(team)).to eq("team-mltv-u15")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "aggiunge un suffisso se lo slug esiste già" do
|
it "aggiunge un suffisso se lo slug esiste già" do
|
||||||
club.teams.create!(name: "Tigers", sport_key: "pallavolo")
|
club.teams.create!(name: "Team MLTV", sport_key: "pallavolo")
|
||||||
team = club.teams.build(name: "Tigers", sport_key: "pallavolo")
|
team = club.teams.build(name: "Team MLTV", sport_key: "pallavolo")
|
||||||
expect(described_class.call(team)).to eq("tigers-2")
|
expect(described_class.call(team)).to eq("team-mltv-2")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+27
-8
@@ -14,6 +14,8 @@ import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
|
|
||||||
|
class SessionApiException(message: String, cause: Throwable) : Exception(message, cause)
|
||||||
|
|
||||||
class SessionRepository(
|
class SessionRepository(
|
||||||
private val api: MatchLiveApi,
|
private val api: MatchLiveApi,
|
||||||
private val appContext: Context,
|
private val appContext: Context,
|
||||||
@@ -54,17 +56,15 @@ class SessionRepository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseErrorCode(error: HttpException): String? {
|
|
||||||
val body = error.response()?.errorBody()?.string().orEmpty()
|
|
||||||
if (body.isBlank()) return null
|
|
||||||
return runCatching { errorAdapter.fromJson(body)?.errorCode }.getOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun fetchSession(sessionId: String): StreamSession =
|
suspend fun fetchSession(sessionId: String): StreamSession =
|
||||||
api.session(sessionId).toDomain()
|
api.session(sessionId).toDomain()
|
||||||
|
|
||||||
suspend fun startSession(sessionId: String): StreamSession =
|
suspend fun startSession(sessionId: String): StreamSession =
|
||||||
api.startSession(sessionId).toDomain()
|
try {
|
||||||
|
api.startSession(sessionId).toDomain()
|
||||||
|
} catch (e: HttpException) {
|
||||||
|
throw SessionApiException(parseErrorMessage(e), e)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun stopSession(sessionId: String): StreamSession =
|
suspend fun stopSession(sessionId: String): StreamSession =
|
||||||
api.stopSession(sessionId).toDomain()
|
api.stopSession(sessionId).toDomain()
|
||||||
@@ -73,7 +73,11 @@ class SessionRepository(
|
|||||||
api.pauseSession(sessionId).toDomain()
|
api.pauseSession(sessionId).toDomain()
|
||||||
|
|
||||||
suspend fun resumeSession(sessionId: String): StreamSession =
|
suspend fun resumeSession(sessionId: String): StreamSession =
|
||||||
api.resumeSession(sessionId).toDomain()
|
try {
|
||||||
|
api.resumeSession(sessionId).toDomain()
|
||||||
|
} catch (e: HttpException) {
|
||||||
|
throw SessionApiException(parseErrorMessage(e), e)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun setAudioMute(sessionId: String, muted: Boolean): StreamSession =
|
suspend fun setAudioMute(sessionId: String, muted: Boolean): StreamSession =
|
||||||
api.setAudioMute(sessionId, com.matchlivetv.match_live_tv.data.api.AudioMuteRequest(muted)).toDomain()
|
api.setAudioMute(sessionId, com.matchlivetv.match_live_tv.data.api.AudioMuteRequest(muted)).toDomain()
|
||||||
@@ -121,4 +125,19 @@ class SessionRepository(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun parseErrorCode(error: HttpException): String? {
|
||||||
|
return parseError(error)?.errorCode
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseErrorMessage(error: HttpException): String {
|
||||||
|
val parsed = parseError(error)
|
||||||
|
return parsed?.error?.takeIf { it.isNotBlank() } ?: error.message() ?: "HTTP ${error.code()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseError(error: HttpException): ApiErrorResponse? {
|
||||||
|
val body = error.response()?.errorBody()?.string().orEmpty()
|
||||||
|
if (body.isBlank()) return null
|
||||||
|
return runCatching { errorAdapter.fromJson(body) }.getOrNull()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ final class MatchScoringRulesTests: XCTestCase {
|
|||||||
Match(
|
Match(
|
||||||
id: "match-1",
|
id: "match-1",
|
||||||
teamId: "team-1",
|
teamId: "team-1",
|
||||||
teamName: "Tigers Volley",
|
teamName: "Team MLTV",
|
||||||
opponentName: "Avversario",
|
opponentName: "Team Guest",
|
||||||
location: nil,
|
location: nil,
|
||||||
scheduledAt: nil,
|
scheduledAt: nil,
|
||||||
sportKey: "pallavolo",
|
sportKey: "pallavolo",
|
||||||
|
|||||||
Reference in New Issue
Block a user