Migliora sessioni admin e rende robusto il logout web.
Aggiunge filtri/colonne (società, orari) e dettaglio leggibile; evita 422 CSRF su logout e non cancella le cover slate custom in sync prod. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
module Admin
|
module Admin
|
||||||
class AuthController < Admin::BaseController
|
class AuthController < Admin::BaseController
|
||||||
skip_before_action :require_admin_login, only: %i[new create]
|
skip_before_action :require_admin_login, only: %i[new create]
|
||||||
|
# Same as public logout: tolerate stale CSRF after long-lived admin tabs.
|
||||||
|
skip_before_action :verify_authenticity_token, only: :destroy
|
||||||
|
|
||||||
def new
|
def new
|
||||||
redirect_to admin_root_path if admin_logged_in?
|
redirect_to admin_root_path if admin_logged_in?
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
module Admin
|
module Admin
|
||||||
class SessionsController < Admin::BaseController
|
class SessionsController < Admin::BaseController
|
||||||
def index
|
def index
|
||||||
@sessions = StreamSession.includes(:stream_node, :user, match: :team).order(created_at: :desc).limit(50)
|
@filters = session_filters
|
||||||
|
scope = filtered_sessions
|
||||||
|
@total_count = scope.count
|
||||||
|
@sessions = scope
|
||||||
|
.includes(:stream_node, :user, match: { team: :club })
|
||||||
|
.order(Arel.sql("COALESCE(stream_sessions.started_at, stream_sessions.created_at) DESC"))
|
||||||
|
.limit(100)
|
||||||
|
@clubs = Club.order(:name)
|
||||||
|
@nodes = StreamNode.order(:slug)
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@session = StreamSession.includes(:stream_node, match: :team).find(params[:id])
|
@session = StreamSession
|
||||||
@events = @session.stream_events.recent.limit(50)
|
.includes(:stream_node, :user, :device_states, :recording, match: { team: :club })
|
||||||
|
.find(params[:id])
|
||||||
|
@events = @session.stream_events.recent.limit(100)
|
||||||
|
@club = @session.match.team.club
|
||||||
end
|
end
|
||||||
|
|
||||||
def stop
|
def stop
|
||||||
@@ -39,5 +50,66 @@ module Admin
|
|||||||
regia_expires_at: @session.regia_token_expires_at&.iso8601
|
regia_expires_at: @session.regia_token_expires_at&.iso8601
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def session_filters
|
||||||
|
{
|
||||||
|
q: params[:q].to_s.strip.presence,
|
||||||
|
status: params[:status].to_s.strip.presence,
|
||||||
|
platform: params[:platform].to_s.strip.presence,
|
||||||
|
club_id: params[:club_id].to_s.strip.presence,
|
||||||
|
stream_node_id: params[:stream_node_id].to_s.strip.presence,
|
||||||
|
from: params[:from].to_s.strip.presence,
|
||||||
|
to: params[:to].to_s.strip.presence
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def filtered_sessions
|
||||||
|
scope = StreamSession.left_outer_joins(match: { team: :club })
|
||||||
|
|
||||||
|
if @filters[:q].present?
|
||||||
|
term = "%#{ActiveRecord::Base.sanitize_sql_like(@filters[:q])}%"
|
||||||
|
scope = scope.where(
|
||||||
|
"teams.name ILIKE :term OR matches.opponent_name ILIKE :term OR clubs.name ILIKE :term OR matches.location ILIKE :term",
|
||||||
|
term: term
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
if @filters[:status].present? && StreamSession::STATUSES.include?(@filters[:status])
|
||||||
|
scope = scope.where(stream_sessions: { status: @filters[:status] })
|
||||||
|
end
|
||||||
|
if @filters[:platform].present? && StreamSession::PLATFORMS.include?(@filters[:platform])
|
||||||
|
scope = scope.where(stream_sessions: { platform: @filters[:platform] })
|
||||||
|
end
|
||||||
|
if @filters[:club_id].present?
|
||||||
|
scope = scope.where(teams: { club_id: @filters[:club_id] })
|
||||||
|
end
|
||||||
|
if @filters[:stream_node_id].present?
|
||||||
|
scope = scope.where(stream_sessions: { stream_node_id: @filters[:stream_node_id] })
|
||||||
|
end
|
||||||
|
if (from_time = parse_filter_date(@filters[:from], end_of_day: false))
|
||||||
|
scope = scope.where(
|
||||||
|
"COALESCE(stream_sessions.started_at, stream_sessions.created_at) >= ?",
|
||||||
|
from_time
|
||||||
|
)
|
||||||
|
end
|
||||||
|
if (to_time = parse_filter_date(@filters[:to], end_of_day: true))
|
||||||
|
scope = scope.where(
|
||||||
|
"COALESCE(stream_sessions.started_at, stream_sessions.created_at) <= ?",
|
||||||
|
to_time
|
||||||
|
)
|
||||||
|
end
|
||||||
|
scope
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_filter_date(value, end_of_day:)
|
||||||
|
return nil if value.blank?
|
||||||
|
|
||||||
|
date = Date.parse(value)
|
||||||
|
end_of_day ? date.end_of_day : date.beginning_of_day
|
||||||
|
rescue ArgumentError, TypeError
|
||||||
|
nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
module Public
|
module Public
|
||||||
class SessionsController < WebBaseController
|
class SessionsController < WebBaseController
|
||||||
|
# Logout must succeed even with a stale CSRF token (old tab after deploy /
|
||||||
|
# session rotate). SameSite=Lax already blocks cross-site cookie POSTs.
|
||||||
|
skip_before_action :verify_authenticity_token, only: :destroy
|
||||||
|
|
||||||
def new
|
def new
|
||||||
if logged_in? && current_user.primary_club
|
if logged_in? && current_user.primary_club
|
||||||
redirect_to public_club_path(current_user.primary_club)
|
redirect_to public_club_path(current_user.primary_club)
|
||||||
|
|||||||
@@ -43,6 +43,75 @@ module AdminHelper
|
|||||||
I18n.t("admin.sessions.ingest.role.#{node.role}", default: node.role.to_s.humanize)
|
I18n.t("admin.sessions.ingest.role.#{node.role}", default: node.role.to_s.humanize)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def admin_session_status_badge_class(status)
|
||||||
|
case status.to_s
|
||||||
|
when "live" then "badge--live"
|
||||||
|
when "connecting", "reconnecting" then "badge--connecting"
|
||||||
|
when "paused", "idle" then "badge--paused"
|
||||||
|
when "ended" then "badge--ended"
|
||||||
|
when "error" then "badge--error"
|
||||||
|
else "badge--paused"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def admin_datetime(time, with_seconds: false)
|
||||||
|
return I18n.t("admin.common.dash") if time.blank?
|
||||||
|
|
||||||
|
format = with_seconds ? "%d/%m/%Y %H:%M:%S" : "%d/%m/%Y %H:%M"
|
||||||
|
time.in_time_zone.strftime(format)
|
||||||
|
end
|
||||||
|
|
||||||
|
def admin_session_duration_label(session)
|
||||||
|
secs = session.total_duration_secs.to_i
|
||||||
|
if secs <= 0 && session.started_at
|
||||||
|
end_time = session.ended_at || (session.terminal? ? session.updated_at : Time.current)
|
||||||
|
secs = (end_time - session.started_at).to_i if end_time
|
||||||
|
end
|
||||||
|
return I18n.t("admin.common.dash") if secs <= 0
|
||||||
|
|
||||||
|
hours = secs / 3600
|
||||||
|
mins = (secs % 3600) / 60
|
||||||
|
rem = secs % 60
|
||||||
|
if hours.positive?
|
||||||
|
format("%dh %02dm %02ds", hours, mins, rem)
|
||||||
|
elsif mins.positive?
|
||||||
|
format("%dm %02ds", mins, rem)
|
||||||
|
else
|
||||||
|
"#{rem}s"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def admin_format_event_meta(metadata)
|
||||||
|
return content_tag(:span, I18n.t("admin.common.dash"), class: "muted") if metadata.blank?
|
||||||
|
|
||||||
|
pairs = metadata.to_h
|
||||||
|
return content_tag(:span, I18n.t("admin.common.dash"), class: "muted") if pairs.empty?
|
||||||
|
|
||||||
|
content_tag(:dl, class: "admin-event-meta") do
|
||||||
|
safe_join(
|
||||||
|
pairs.map do |key, value|
|
||||||
|
content_tag(:div, class: "admin-event-meta__row") do
|
||||||
|
content_tag(:dt, key.to_s) +
|
||||||
|
content_tag(:dd, admin_event_meta_value(value))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def admin_event_meta_value(value)
|
||||||
|
case value
|
||||||
|
when Hash, Array
|
||||||
|
content_tag(:code, value.to_json)
|
||||||
|
when TrueClass, FalseClass
|
||||||
|
value ? "true" : "false"
|
||||||
|
when NilClass
|
||||||
|
I18n.t("admin.common.dash")
|
||||||
|
else
|
||||||
|
value.to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def admin_regia_expires_label(iso_time)
|
def admin_regia_expires_label(iso_time)
|
||||||
return if iso_time.blank?
|
return if iso_time.blank?
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class StreamSession < ApplicationRecord
|
|||||||
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_one :score_state, dependent: :destroy
|
has_one :score_state, dependent: :destroy
|
||||||
|
has_one :recording
|
||||||
has_many :device_states, dependent: :destroy
|
has_many :device_states, dependent: :destroy
|
||||||
|
|
||||||
validates :platform, inclusion: { in: PLATFORMS }
|
validates :platform, inclusion: { in: PLATFORMS }
|
||||||
|
|||||||
@@ -1,9 +1,87 @@
|
|||||||
<h2><%= t("admin.sessions.index.title") %></h2>
|
<% content_for :body_class, "admin-body" %>
|
||||||
<table class="admin-table">
|
|
||||||
|
<div class="admin-page-head">
|
||||||
|
<h2 class="admin-page-title"><%= t("admin.sessions.index.title") %></h2>
|
||||||
|
<p class="muted admin-page-sub"><%= t("admin.sessions.index.lead") %></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel admin-sessions-filters">
|
||||||
|
<%= form_with url: admin_sessions_path, method: :get, local: true, class: "admin-filter-form" do %>
|
||||||
|
<div class="admin-filter-grid">
|
||||||
|
<label class="admin-filter-field">
|
||||||
|
<span><%= t("admin.sessions.index.filters.q") %></span>
|
||||||
|
<%= text_field_tag :q, @filters[:q], placeholder: t("admin.sessions.index.filters.q_placeholder") %>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="admin-filter-field">
|
||||||
|
<span><%= t("admin.sessions.index.filters.status") %></span>
|
||||||
|
<%= select_tag :status,
|
||||||
|
options_for_select(
|
||||||
|
[[t("admin.sessions.index.filters.any"), ""]] + StreamSession::STATUSES.map { |s| [s, s] },
|
||||||
|
@filters[:status]
|
||||||
|
) %>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="admin-filter-field">
|
||||||
|
<span><%= t("admin.sessions.index.filters.platform") %></span>
|
||||||
|
<%= select_tag :platform,
|
||||||
|
options_for_select(
|
||||||
|
[[t("admin.sessions.index.filters.any"), ""]] + StreamSession::PLATFORMS.map { |p| [p, p] },
|
||||||
|
@filters[:platform]
|
||||||
|
) %>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="admin-filter-field">
|
||||||
|
<span><%= t("admin.sessions.index.filters.club") %></span>
|
||||||
|
<%= select_tag :club_id,
|
||||||
|
options_for_select(
|
||||||
|
[[t("admin.sessions.index.filters.any"), ""]] + @clubs.map { |c| [c.name, c.id] },
|
||||||
|
@filters[:club_id]
|
||||||
|
) %>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="admin-filter-field">
|
||||||
|
<span><%= t("admin.sessions.index.filters.node") %></span>
|
||||||
|
<%= select_tag :stream_node_id,
|
||||||
|
options_for_select(
|
||||||
|
[[t("admin.sessions.index.filters.any"), ""]] + @nodes.map { |n| ["#{n.slug} (#{n.role})", n.id] },
|
||||||
|
@filters[:stream_node_id]
|
||||||
|
) %>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="admin-filter-field">
|
||||||
|
<span><%= t("admin.sessions.index.filters.from") %></span>
|
||||||
|
<%= date_field_tag :from, @filters[:from] %>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="admin-filter-field">
|
||||||
|
<span><%= t("admin.sessions.index.filters.to") %></span>
|
||||||
|
<%= date_field_tag :to, @filters[:to] %>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-filter-actions">
|
||||||
|
<%= submit_tag t("admin.sessions.index.filters.apply"), class: "admin-btn admin-btn--primary admin-btn--sm" %>
|
||||||
|
<%= link_to t("admin.sessions.index.filters.reset"), admin_sessions_path, class: "admin-btn admin-btn--outline admin-btn--sm" %>
|
||||||
|
<span class="muted admin-filter-count">
|
||||||
|
<%= t("admin.sessions.index.results", shown: @sessions.size, total: @total_count) %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<% if @sessions.any? %>
|
||||||
|
<div class="admin-table-wrap">
|
||||||
|
<table class="admin-table admin-table--sessions">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th><%= t("admin.sessions.index.table.club") %></th>
|
||||||
<th><%= t("admin.sessions.index.table.match") %></th>
|
<th><%= t("admin.sessions.index.table.match") %></th>
|
||||||
<th><%= t("admin.sessions.index.table.status") %></th>
|
<th><%= t("admin.sessions.index.table.status") %></th>
|
||||||
|
<th><%= t("admin.sessions.index.table.started") %></th>
|
||||||
|
<th><%= t("admin.sessions.index.table.ended") %></th>
|
||||||
|
<th><%= t("admin.sessions.index.table.duration") %></th>
|
||||||
<th><%= t("admin.sessions.index.table.ingest") %></th>
|
<th><%= t("admin.sessions.index.table.ingest") %></th>
|
||||||
<th><%= t("admin.sessions.index.table.disconnects") %></th>
|
<th><%= t("admin.sessions.index.table.disconnects") %></th>
|
||||||
<th><%= t("admin.sessions.index.table.link") %></th>
|
<th><%= t("admin.sessions.index.table.link") %></th>
|
||||||
@@ -12,9 +90,25 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<% @sessions.each do |s| %>
|
<% @sessions.each do |s| %>
|
||||||
|
<% club = s.match.team.club %>
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= s.match.team.name %> vs <%= s.match.opponent_name %></td>
|
<td>
|
||||||
<td><span class="badge <%= s.status == 'live' ? 'badge--live' : 'badge--paused' %>"><%= s.status %></span></td>
|
<% if club %>
|
||||||
|
<%= link_to club.name, admin_club_path(club), class: "admin-table-strong" %>
|
||||||
|
<% else %>
|
||||||
|
<span class="muted"><%= t("admin.common.dash") %></span>
|
||||||
|
<% end %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="admin-table-strong"><%= s.match.team.name %> vs <%= s.match.opponent_name %></div>
|
||||||
|
<div class="muted admin-table-sub"><%= s.platform %></div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge <%= admin_session_status_badge_class(s.status) %>"><%= s.status %></span>
|
||||||
|
</td>
|
||||||
|
<td class="muted"><%= admin_datetime(s.started_at || s.created_at) %></td>
|
||||||
|
<td class="muted"><%= s.ended_at ? admin_datetime(s.ended_at) : t("admin.common.dash") %></td>
|
||||||
|
<td class="muted"><%= admin_session_duration_label(s) %></td>
|
||||||
<td><%= render "admin/sessions/ingest_cell", session: s %></td>
|
<td><%= render "admin/sessions/ingest_cell", session: s %></td>
|
||||||
<td><%= s.disconnection_count %></td>
|
<td><%= s.disconnection_count %></td>
|
||||||
<td>
|
<td>
|
||||||
@@ -29,4 +123,9 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<% end %>
|
<% end %>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<p class="empty"><%= t("admin.sessions.index.none") %></p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -1,35 +1,228 @@
|
|||||||
<h2><%= t("admin.sessions.show.title", id: @session.id) %></h2>
|
<% content_for :body_class, "admin-body" %>
|
||||||
<p><%= t("admin.sessions.show.status_label") %> <strong><%= @session.status %></strong></p>
|
<% team = @session.match.team %>
|
||||||
<% unless @session.terminal? %>
|
<% club = @club || team.club %>
|
||||||
<p>
|
|
||||||
|
<div class="admin-page-head admin-session-head">
|
||||||
|
<div>
|
||||||
|
<p class="muted admin-page-sub">
|
||||||
|
<%= link_to t("admin.sessions.show.back"), admin_sessions_path %>
|
||||||
|
</p>
|
||||||
|
<h2 class="admin-page-title"><%= t("admin.sessions.show.heading") %></h2>
|
||||||
|
<p class="admin-session-match">
|
||||||
|
<strong><%= team.name %></strong>
|
||||||
|
<span class="muted">vs</span>
|
||||||
|
<strong><%= @session.match.opponent_name %></strong>
|
||||||
|
</p>
|
||||||
|
<% if club %>
|
||||||
|
<p class="muted">
|
||||||
|
<%= t("admin.sessions.show.club_label") %>
|
||||||
|
<%= link_to club.name, admin_club_path(club) %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<div class="admin-session-head-actions">
|
||||||
|
<span class="badge <%= admin_session_status_badge_class(@session.status) %>"><%= @session.status %></span>
|
||||||
|
<% unless @session.terminal? %>
|
||||||
<%= button_to t("admin.sessions.show.stop_button"),
|
<%= button_to t("admin.sessions.show.stop_button"),
|
||||||
stop_admin_session_path(@session),
|
stop_admin_session_path(@session),
|
||||||
method: :post,
|
method: :post,
|
||||||
class: "admin-btn admin-btn--danger",
|
class: "admin-btn admin-btn--danger admin-btn--sm",
|
||||||
form: { data: { turbo_confirm: t("admin.sessions.show.stop_confirm") } } %>
|
form: { data: { turbo_confirm: t("admin.sessions.show.stop_confirm") } } %>
|
||||||
</p>
|
<% end %>
|
||||||
<% end %>
|
</div>
|
||||||
<p><%= t("admin.sessions.show.match_label", team: @session.match.team.name, opponent: @session.match.opponent_name) %></p>
|
</div>
|
||||||
|
|
||||||
<%= render "admin/sessions/links", session: @session %>
|
<div class="admin-session-grid">
|
||||||
|
<section class="panel">
|
||||||
|
<h3><%= t("admin.sessions.show.summary_title") %></h3>
|
||||||
|
<dl class="admin-kv">
|
||||||
|
<div>
|
||||||
|
<dt><%= t("admin.sessions.show.fields.id") %></dt>
|
||||||
|
<dd><code class="admin-mono"><%= @session.id %></code></dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt><%= t("admin.sessions.show.fields.club") %></dt>
|
||||||
|
<dd><%= club&.name || t("admin.common.dash") %></dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt><%= t("admin.sessions.show.fields.team") %></dt>
|
||||||
|
<dd><%= team.name %></dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt><%= t("admin.sessions.show.fields.opponent") %></dt>
|
||||||
|
<dd><%= @session.match.opponent_name %></dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt><%= t("admin.sessions.show.fields.operator") %></dt>
|
||||||
|
<dd>
|
||||||
|
<% if @session.user %>
|
||||||
|
<%= @session.user.name.presence || @session.user.email %>
|
||||||
|
<span class="muted">(<%= @session.user.email %>)</span>
|
||||||
|
<% else %>
|
||||||
|
<%= t("admin.common.dash") %>
|
||||||
|
<% end %>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt><%= t("admin.sessions.show.fields.platform") %></dt>
|
||||||
|
<dd><%= @session.platform %></dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt><%= t("admin.sessions.show.fields.privacy") %></dt>
|
||||||
|
<dd><%= @session.privacy_status %></dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt><%= t("admin.sessions.show.fields.quality") %></dt>
|
||||||
|
<dd><%= @session.quality_preset %></dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt><%= t("admin.sessions.show.fields.min_quality") %></dt>
|
||||||
|
<dd><%= @session.min_quality_preset %></dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt><%= t("admin.sessions.show.fields.audio") %></dt>
|
||||||
|
<dd><%= @session.audio_muted? ? t("admin.sessions.show.audio_muted") : t("admin.sessions.show.audio_on") %></dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
|
||||||
<% if @session.youtube_broadcast_id %>
|
<section class="panel">
|
||||||
<p><%= t("admin.sessions.show.youtube_studio") %>: <a href="https://studio.youtube.com/video/<%= @session.youtube_broadcast_id %>/livestreaming" target="_blank" rel="noopener"><%= t("admin.sessions.show.broadcast") %></a></p>
|
<h3><%= t("admin.sessions.show.timing_title") %></h3>
|
||||||
<% end %>
|
<dl class="admin-kv">
|
||||||
<p>
|
<div>
|
||||||
<%= t("admin.sessions.show.ingest_node") %>
|
<dt><%= t("admin.sessions.show.fields.created") %></dt>
|
||||||
|
<dd><%= admin_datetime(@session.created_at, with_seconds: true) %></dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt><%= t("admin.sessions.show.fields.started") %></dt>
|
||||||
|
<dd><%= admin_datetime(@session.started_at, with_seconds: true) %></dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt><%= t("admin.sessions.show.fields.ended") %></dt>
|
||||||
|
<dd><%= admin_datetime(@session.ended_at, with_seconds: true) %></dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt><%= t("admin.sessions.show.fields.duration") %></dt>
|
||||||
|
<dd><%= admin_session_duration_label(@session) %></dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt><%= t("admin.sessions.show.fields.disconnects") %></dt>
|
||||||
|
<dd><%= @session.disconnection_count %></dd>
|
||||||
|
</div>
|
||||||
|
<% if @session.match.scheduled_at %>
|
||||||
|
<div>
|
||||||
|
<dt><%= t("admin.sessions.show.fields.scheduled") %></dt>
|
||||||
|
<dd><%= admin_datetime(@session.match.scheduled_at) %></dd>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% if @session.match.location.present? %>
|
||||||
|
<div>
|
||||||
|
<dt><%= t("admin.sessions.show.fields.location") %></dt>
|
||||||
|
<dd><%= @session.match.location %></dd>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h3><%= t("admin.sessions.show.ingest_title") %></h3>
|
||||||
|
<dl class="admin-kv">
|
||||||
|
<div>
|
||||||
|
<dt><%= t("admin.sessions.show.fields.node") %></dt>
|
||||||
|
<dd>
|
||||||
<% if @session.stream_node %>
|
<% if @session.stream_node %>
|
||||||
<code><%= @session.stream_node.slug %></code>
|
<code><%= @session.stream_node.slug %></code>
|
||||||
<span class="badge <%= admin_session_ingest_badge_class(@session.stream_node) %>"><%= admin_session_ingest_role_label(@session.stream_node) %></span>
|
<span class="badge <%= admin_session_ingest_badge_class(@session.stream_node) %>">
|
||||||
|
<%= admin_session_ingest_role_label(@session.stream_node) %>
|
||||||
|
</span>
|
||||||
<span class="muted">(<%= @session.stream_node.provider %>)</span>
|
<span class="muted">(<%= @session.stream_node.provider %>)</span>
|
||||||
<% else %>
|
<% else %>
|
||||||
<span class="muted"><%= t("admin.sessions.ingest.none") %></span>
|
<span class="muted"><%= t("admin.sessions.ingest.none") %></span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</p>
|
</dd>
|
||||||
<p><%= t("admin.sessions.show.rtmp_ingest") %> <code><%= @session.rtmp_ingest_url %></code></p>
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt><%= t("admin.sessions.show.fields.rtmp") %></dt>
|
||||||
|
<dd><code class="admin-mono admin-mono--wrap"><%= @session.rtmp_ingest_url %></code></dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt><%= t("admin.sessions.show.fields.hls") %></dt>
|
||||||
|
<dd><code class="admin-mono admin-mono--wrap"><%= @session.hls_playback_url %></code></dd>
|
||||||
|
</div>
|
||||||
|
<% if @session.youtube_broadcast_id.present? %>
|
||||||
|
<div>
|
||||||
|
<dt><%= t("admin.sessions.show.youtube_studio") %></dt>
|
||||||
|
<dd>
|
||||||
|
<a href="https://studio.youtube.com/video/<%= @session.youtube_broadcast_id %>/livestreaming" target="_blank" rel="noopener">
|
||||||
|
<%= t("admin.sessions.show.broadcast") %>
|
||||||
|
</a>
|
||||||
|
<span class="muted">(<%= @session.youtube_broadcast_id %>)</span>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% if @session.recording %>
|
||||||
|
<div>
|
||||||
|
<dt><%= t("admin.sessions.show.fields.recording") %></dt>
|
||||||
|
<dd>
|
||||||
|
<span class="badge badge--<%= @session.recording.status == 'ready' ? 'ready' : 'paused' %>">
|
||||||
|
<%= @session.recording.status %>
|
||||||
|
</span>
|
||||||
|
<% if club %>
|
||||||
|
<%= link_to t("admin.sessions.show.open_replays"), admin_club_recordings_path(club) %>
|
||||||
|
<% end %>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3><%= t("admin.sessions.show.events_title") %></h3>
|
<%= render "admin/sessions/links", session: @session %>
|
||||||
<table>
|
|
||||||
|
<% if @session.device_states.any? %>
|
||||||
|
<section class="panel">
|
||||||
|
<h3><%= t("admin.sessions.show.devices_title") %></h3>
|
||||||
|
<div class="admin-table-wrap">
|
||||||
|
<table class="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><%= t("admin.sessions.show.devices.role") %></th>
|
||||||
|
<th><%= t("admin.sessions.show.devices.network") %></th>
|
||||||
|
<th><%= t("admin.sessions.show.devices.battery") %></th>
|
||||||
|
<th><%= t("admin.sessions.show.devices.bitrate") %></th>
|
||||||
|
<th><%= t("admin.sessions.show.devices.fps") %></th>
|
||||||
|
<th><%= t("admin.sessions.show.devices.last_seen") %></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% @session.device_states.order(:device_role).each do |device| %>
|
||||||
|
<tr>
|
||||||
|
<td><%= device.device_role %></td>
|
||||||
|
<td class="muted"><%= device.network_type.presence || t("admin.common.dash") %></td>
|
||||||
|
<td class="muted"><%= device.battery_level.nil? ? t("admin.common.dash") : "#{device.battery_level}%" %></td>
|
||||||
|
<td class="muted">
|
||||||
|
<% if device.current_bitrate || device.target_bitrate %>
|
||||||
|
<%= device.current_bitrate || t("admin.common.dash") %>
|
||||||
|
<span class="muted">/ <%= device.target_bitrate || t("admin.common.dash") %></span>
|
||||||
|
<% else %>
|
||||||
|
<%= t("admin.common.dash") %>
|
||||||
|
<% end %>
|
||||||
|
</td>
|
||||||
|
<td class="muted"><%= device.fps || t("admin.common.dash") %></td>
|
||||||
|
<td class="muted"><%= admin_datetime(device.last_seen_at, with_seconds: true) %></td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h3><%= t("admin.sessions.show.events_title") %></h3>
|
||||||
|
<% if @events.any? %>
|
||||||
|
<div class="admin-table-wrap">
|
||||||
|
<table class="admin-table admin-table--events">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th><%= t("admin.sessions.show.table.type") %></th>
|
<th><%= t("admin.sessions.show.table.type") %></th>
|
||||||
@@ -40,10 +233,15 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<% @events.each do |e| %>
|
<% @events.each do |e| %>
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= e.event_type %></td>
|
<td><span class="admin-event-type"><%= e.event_type %></span></td>
|
||||||
<td><%= e.occurred_at %></td>
|
<td class="muted"><%= admin_datetime(e.occurred_at, with_seconds: true) %></td>
|
||||||
<td><%= e.metadata.to_json %></td>
|
<td><%= admin_format_event_meta(e.metadata) %></td>
|
||||||
</tr>
|
</tr>
|
||||||
<% end %>
|
<% end %>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<p class="empty"><%= t("admin.sessions.show.events_none") %></p>
|
||||||
|
<% end %>
|
||||||
|
</section>
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
<title><%= t("admin.layout.title") %></title>
|
<title><%= t("admin.layout.title") %></title>
|
||||||
<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">
|
||||||
<link rel="stylesheet" href="/admin.css?v=11">
|
<%= csrf_meta_tags %>
|
||||||
|
<link rel="stylesheet" href="/admin.css?v=12">
|
||||||
<% 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">
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title><%= content_for?(:title) ? yield(:title) : "Match Live TV" %></title>
|
<title><%= content_for?(:title) ? yield(:title) : "Match Live TV" %></title>
|
||||||
<meta name="application-name" content="Match Live TV">
|
<meta name="application-name" content="Match Live TV">
|
||||||
|
<%= csrf_meta_tags %>
|
||||||
<%= render "shared/meta_tags" %>
|
<%= render "shared/meta_tags" %>
|
||||||
<%= 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">
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title><%= content_for?(:title) ? yield(:title) : "Match Live TV" %></title>
|
<title><%= content_for?(:title) ? yield(:title) : "Match Live TV" %></title>
|
||||||
|
<%= csrf_meta_tags %>
|
||||||
<%= render "shared/meta_tags" %>
|
<%= render "shared/meta_tags" %>
|
||||||
<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=68">
|
<link rel="stylesheet" href="/marketing.css?v=68">
|
||||||
|
|||||||
@@ -251,9 +251,28 @@ de:
|
|||||||
sessions:
|
sessions:
|
||||||
index:
|
index:
|
||||||
title: Stream-Sitzungen
|
title: Stream-Sitzungen
|
||||||
|
lead: Suche nach Verein, Team, Gegner oder Ort. Filtere nach Status, Plattform, Knoten und Zeitraum.
|
||||||
|
results: "%{shown} von %{total} angezeigt"
|
||||||
|
none: Keine Sitzungen für diese Filter.
|
||||||
|
filters:
|
||||||
|
q: Suche
|
||||||
|
q_placeholder: Verein, Team, Gegner…
|
||||||
|
status: Status
|
||||||
|
platform: Plattform
|
||||||
|
club: Verein
|
||||||
|
node: Ingest-Knoten
|
||||||
|
from: Von
|
||||||
|
to: Bis
|
||||||
|
any: Alle
|
||||||
|
apply: Filtern
|
||||||
|
reset: Zurücksetzen
|
||||||
table:
|
table:
|
||||||
|
club: Verein
|
||||||
match: Spiel
|
match: Spiel
|
||||||
status: Status
|
status: Status
|
||||||
|
started: Start
|
||||||
|
ended: Ende
|
||||||
|
duration: Dauer
|
||||||
ingest: Ingest
|
ingest: Ingest
|
||||||
disconnects: Verbindungsabbrüche
|
disconnects: Verbindungsabbrüche
|
||||||
link: Link
|
link: Link
|
||||||
@@ -261,19 +280,59 @@ de:
|
|||||||
regia: Regie
|
regia: Regie
|
||||||
show:
|
show:
|
||||||
title: "Sitzung %{id}"
|
title: "Sitzung %{id}"
|
||||||
|
heading: Sitzungsdetails
|
||||||
|
back: "← Zurück zu Sitzungen"
|
||||||
|
club_label: "Verein:"
|
||||||
|
summary_title: Übersicht
|
||||||
|
timing_title: Zeiten
|
||||||
|
ingest_title: Ingest & Medien
|
||||||
|
devices_title: Geräte
|
||||||
|
events_title: Ereignisse
|
||||||
|
events_none: Keine Ereignisse erfasst.
|
||||||
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."
|
||||||
match_label: "Spiel: %{team} gegen %{opponent}"
|
match_label: "Spiel: %{team} gegen %{opponent}"
|
||||||
youtube_studio: YouTube Studio
|
youtube_studio: YouTube Studio
|
||||||
broadcast: Übertragung
|
broadcast: Übertragung
|
||||||
|
open_replays: Vereins-Replays öffnen
|
||||||
|
audio_muted: Audio stumm
|
||||||
|
audio_on: Audio an
|
||||||
ingest_node: "Ingest-Knoten:"
|
ingest_node: "Ingest-Knoten:"
|
||||||
rtmp_ingest: "RTMP-Ingest:"
|
rtmp_ingest: "RTMP-Ingest:"
|
||||||
events_title: Ereignisse
|
fields:
|
||||||
|
id: ID
|
||||||
|
club: Verein
|
||||||
|
team: Team
|
||||||
|
opponent: Gegner
|
||||||
|
operator: Operator
|
||||||
|
platform: Plattform
|
||||||
|
privacy: Privacy
|
||||||
|
quality: Qualität
|
||||||
|
min_quality: Min. Qualität
|
||||||
|
audio: Audio
|
||||||
|
created: Erstellt
|
||||||
|
started: Start
|
||||||
|
ended: Ende
|
||||||
|
duration: Dauer
|
||||||
|
disconnects: Abbrüche
|
||||||
|
scheduled: Geplantes Spiel
|
||||||
|
location: Ort
|
||||||
|
node: Knoten
|
||||||
|
rtmp: RTMP-Ingest
|
||||||
|
hls: HLS
|
||||||
|
recording: Replay
|
||||||
|
devices:
|
||||||
|
role: Rolle
|
||||||
|
network: Netz
|
||||||
|
battery: Akku
|
||||||
|
bitrate: Bitrate
|
||||||
|
fps: FPS
|
||||||
|
last_seen: Zuletzt gesehen
|
||||||
table:
|
table:
|
||||||
type: Typ
|
type: Typ
|
||||||
when: Wann
|
when: Wann
|
||||||
meta: Meta
|
meta: Details
|
||||||
links:
|
links:
|
||||||
watch_title: Live-Link
|
watch_title: Live-Link
|
||||||
none: Kein Video-Link für diese Sitzung verfügbar.
|
none: Kein Video-Link für diese Sitzung verfügbar.
|
||||||
|
|||||||
@@ -251,9 +251,28 @@ en:
|
|||||||
sessions:
|
sessions:
|
||||||
index:
|
index:
|
||||||
title: Stream sessions
|
title: Stream sessions
|
||||||
|
lead: Search by club, team, opponent or venue. Filter by status, platform, node and date range.
|
||||||
|
results: "Showing %{shown} of %{total}"
|
||||||
|
none: No sessions match these filters.
|
||||||
|
filters:
|
||||||
|
q: Search
|
||||||
|
q_placeholder: Club, team, opponent…
|
||||||
|
status: Status
|
||||||
|
platform: Platform
|
||||||
|
club: Club
|
||||||
|
node: Ingest node
|
||||||
|
from: From
|
||||||
|
to: To
|
||||||
|
any: All
|
||||||
|
apply: Filter
|
||||||
|
reset: Reset
|
||||||
table:
|
table:
|
||||||
|
club: Club
|
||||||
match: Match
|
match: Match
|
||||||
status: Status
|
status: Status
|
||||||
|
started: Started
|
||||||
|
ended: Ended
|
||||||
|
duration: Duration
|
||||||
ingest: Ingest
|
ingest: Ingest
|
||||||
disconnects: Disconnects
|
disconnects: Disconnects
|
||||||
link: Link
|
link: Link
|
||||||
@@ -261,19 +280,59 @@ en:
|
|||||||
regia: Control
|
regia: Control
|
||||||
show:
|
show:
|
||||||
title: "Session %{id}"
|
title: "Session %{id}"
|
||||||
|
heading: Session details
|
||||||
|
back: "← Back to sessions"
|
||||||
|
club_label: "Club:"
|
||||||
|
summary_title: Summary
|
||||||
|
timing_title: Timing
|
||||||
|
ingest_title: Ingest & media
|
||||||
|
devices_title: Devices
|
||||||
|
events_title: Events
|
||||||
|
events_none: No events recorded.
|
||||||
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."
|
||||||
match_label: "Match: %{team} vs %{opponent}"
|
match_label: "Match: %{team} vs %{opponent}"
|
||||||
youtube_studio: YouTube Studio
|
youtube_studio: YouTube Studio
|
||||||
broadcast: Broadcast
|
broadcast: Broadcast
|
||||||
|
open_replays: Open club replays
|
||||||
|
audio_muted: Audio muted
|
||||||
|
audio_on: Audio on
|
||||||
ingest_node: "Ingest node:"
|
ingest_node: "Ingest node:"
|
||||||
rtmp_ingest: "RTMP ingest:"
|
rtmp_ingest: "RTMP ingest:"
|
||||||
events_title: Events
|
fields:
|
||||||
|
id: ID
|
||||||
|
club: Club
|
||||||
|
team: Team
|
||||||
|
opponent: Opponent
|
||||||
|
operator: Operator
|
||||||
|
platform: Platform
|
||||||
|
privacy: Privacy
|
||||||
|
quality: Quality
|
||||||
|
min_quality: Min quality
|
||||||
|
audio: Audio
|
||||||
|
created: Created
|
||||||
|
started: Started
|
||||||
|
ended: Ended
|
||||||
|
duration: Duration
|
||||||
|
disconnects: Disconnects
|
||||||
|
scheduled: Scheduled match
|
||||||
|
location: Venue
|
||||||
|
node: Node
|
||||||
|
rtmp: RTMP ingest
|
||||||
|
hls: HLS
|
||||||
|
recording: Replay
|
||||||
|
devices:
|
||||||
|
role: Role
|
||||||
|
network: Network
|
||||||
|
battery: Battery
|
||||||
|
bitrate: Bitrate
|
||||||
|
fps: FPS
|
||||||
|
last_seen: Last seen
|
||||||
table:
|
table:
|
||||||
type: Type
|
type: Type
|
||||||
when: When
|
when: When
|
||||||
meta: Meta
|
meta: Details
|
||||||
links:
|
links:
|
||||||
watch_title: Live link
|
watch_title: Live link
|
||||||
none: No video link available for this session.
|
none: No video link available for this session.
|
||||||
|
|||||||
@@ -251,9 +251,28 @@ es:
|
|||||||
sessions:
|
sessions:
|
||||||
index:
|
index:
|
||||||
title: Sesiones de emisión
|
title: Sesiones de emisión
|
||||||
|
lead: Busca por club, equipo, rival o sede. Filtra por estado, plataforma, nodo y periodo.
|
||||||
|
results: "Mostrando %{shown} de %{total}"
|
||||||
|
none: Ninguna sesión coincide con estos filtros.
|
||||||
|
filters:
|
||||||
|
q: Buscar
|
||||||
|
q_placeholder: Club, equipo, rival…
|
||||||
|
status: Estado
|
||||||
|
platform: Plataforma
|
||||||
|
club: Club
|
||||||
|
node: Nodo ingest
|
||||||
|
from: Desde
|
||||||
|
to: Hasta
|
||||||
|
any: Todos
|
||||||
|
apply: Filtrar
|
||||||
|
reset: Restablecer
|
||||||
table:
|
table:
|
||||||
|
club: Club
|
||||||
match: Partido
|
match: Partido
|
||||||
status: Estado
|
status: Estado
|
||||||
|
started: Inicio
|
||||||
|
ended: Fin
|
||||||
|
duration: Duración
|
||||||
ingest: Ingest
|
ingest: Ingest
|
||||||
disconnects: Desconexiones
|
disconnects: Desconexiones
|
||||||
link: Enlace
|
link: Enlace
|
||||||
@@ -261,19 +280,59 @@ es:
|
|||||||
regia: Regie
|
regia: Regie
|
||||||
show:
|
show:
|
||||||
title: "Sesión %{id}"
|
title: "Sesión %{id}"
|
||||||
|
heading: Detalle de sesión
|
||||||
|
back: "← Volver a sesiones"
|
||||||
|
club_label: "Club:"
|
||||||
|
summary_title: Resumen
|
||||||
|
timing_title: Tiempos
|
||||||
|
ingest_title: Ingest y medios
|
||||||
|
devices_title: Dispositivos
|
||||||
|
events_title: Eventos
|
||||||
|
events_none: No hay eventos registrados.
|
||||||
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."
|
||||||
match_label: "Partido: %{team} vs %{opponent}"
|
match_label: "Partido: %{team} vs %{opponent}"
|
||||||
youtube_studio: YouTube Studio
|
youtube_studio: YouTube Studio
|
||||||
broadcast: Emisión
|
broadcast: Emisión
|
||||||
|
open_replays: Abrir replays del club
|
||||||
|
audio_muted: Audio silenciado
|
||||||
|
audio_on: Audio activo
|
||||||
ingest_node: "Nodo ingest:"
|
ingest_node: "Nodo ingest:"
|
||||||
rtmp_ingest: "Ingesta RTMP:"
|
rtmp_ingest: "Ingesta RTMP:"
|
||||||
events_title: Eventos
|
fields:
|
||||||
|
id: ID
|
||||||
|
club: Club
|
||||||
|
team: Equipo
|
||||||
|
opponent: Rival
|
||||||
|
operator: Operador
|
||||||
|
platform: Plataforma
|
||||||
|
privacy: Privacidad
|
||||||
|
quality: Calidad
|
||||||
|
min_quality: Calidad mínima
|
||||||
|
audio: Audio
|
||||||
|
created: Creada
|
||||||
|
started: Inicio
|
||||||
|
ended: Fin
|
||||||
|
duration: Duración
|
||||||
|
disconnects: Desconexiones
|
||||||
|
scheduled: Partido programado
|
||||||
|
location: Sede
|
||||||
|
node: Nodo
|
||||||
|
rtmp: Ingesta RTMP
|
||||||
|
hls: HLS
|
||||||
|
recording: Replay
|
||||||
|
devices:
|
||||||
|
role: Rol
|
||||||
|
network: Red
|
||||||
|
battery: Batería
|
||||||
|
bitrate: Bitrate
|
||||||
|
fps: FPS
|
||||||
|
last_seen: Último contacto
|
||||||
table:
|
table:
|
||||||
type: Tipo
|
type: Tipo
|
||||||
when: Cuándo
|
when: Cuándo
|
||||||
meta: Meta
|
meta: Detalles
|
||||||
links:
|
links:
|
||||||
watch_title: Enlace en directo
|
watch_title: Enlace en directo
|
||||||
none: No hay ningún enlace de vídeo disponible para esta sesión.
|
none: No hay ningún enlace de vídeo disponible para esta sesión.
|
||||||
|
|||||||
@@ -251,9 +251,28 @@ fr:
|
|||||||
sessions:
|
sessions:
|
||||||
index:
|
index:
|
||||||
title: Sessions de diffusion
|
title: Sessions de diffusion
|
||||||
|
lead: Recherchez par club, équipe, adversaire ou lieu. Filtrez par statut, plateforme, nœud et période.
|
||||||
|
results: "%{shown} sur %{total} affichées"
|
||||||
|
none: Aucune session ne correspond à ces filtres.
|
||||||
|
filters:
|
||||||
|
q: Recherche
|
||||||
|
q_placeholder: Club, équipe, adversaire…
|
||||||
|
status: Statut
|
||||||
|
platform: Plateforme
|
||||||
|
club: Club
|
||||||
|
node: Nœud ingest
|
||||||
|
from: Du
|
||||||
|
to: Au
|
||||||
|
any: Tous
|
||||||
|
apply: Filtrer
|
||||||
|
reset: Réinitialiser
|
||||||
table:
|
table:
|
||||||
|
club: Club
|
||||||
match: Match
|
match: Match
|
||||||
status: Statut
|
status: Statut
|
||||||
|
started: Début
|
||||||
|
ended: Fin
|
||||||
|
duration: Durée
|
||||||
ingest: Ingest
|
ingest: Ingest
|
||||||
disconnects: Déconnexions
|
disconnects: Déconnexions
|
||||||
link: Lien
|
link: Lien
|
||||||
@@ -261,19 +280,59 @@ fr:
|
|||||||
regia: Régie
|
regia: Régie
|
||||||
show:
|
show:
|
||||||
title: "Session %{id}"
|
title: "Session %{id}"
|
||||||
|
heading: Détail de la session
|
||||||
|
back: "← Retour aux sessions"
|
||||||
|
club_label: "Club :"
|
||||||
|
summary_title: Résumé
|
||||||
|
timing_title: Horaires
|
||||||
|
ingest_title: Ingest et médias
|
||||||
|
devices_title: Appareils
|
||||||
|
events_title: Événements
|
||||||
|
events_none: Aucun événement enregistré.
|
||||||
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é."
|
||||||
match_label: "Match : %{team} contre %{opponent}"
|
match_label: "Match : %{team} contre %{opponent}"
|
||||||
youtube_studio: YouTube Studio
|
youtube_studio: YouTube Studio
|
||||||
broadcast: Diffusion
|
broadcast: Diffusion
|
||||||
|
open_replays: Ouvrir les replays du club
|
||||||
|
audio_muted: Audio coupé
|
||||||
|
audio_on: Audio actif
|
||||||
ingest_node: "Nœud ingest :"
|
ingest_node: "Nœud ingest :"
|
||||||
rtmp_ingest: "Ingestion RTMP :"
|
rtmp_ingest: "Ingestion RTMP :"
|
||||||
events_title: Événements
|
fields:
|
||||||
|
id: ID
|
||||||
|
club: Club
|
||||||
|
team: Équipe
|
||||||
|
opponent: Adversaire
|
||||||
|
operator: Opérateur
|
||||||
|
platform: Plateforme
|
||||||
|
privacy: Confidentialité
|
||||||
|
quality: Qualité
|
||||||
|
min_quality: Qualité mini
|
||||||
|
audio: Audio
|
||||||
|
created: Créée
|
||||||
|
started: Début
|
||||||
|
ended: Fin
|
||||||
|
duration: Durée
|
||||||
|
disconnects: Déconnexions
|
||||||
|
scheduled: Match programmé
|
||||||
|
location: Lieu
|
||||||
|
node: Nœud
|
||||||
|
rtmp: Ingestion RTMP
|
||||||
|
hls: HLS
|
||||||
|
recording: Replay
|
||||||
|
devices:
|
||||||
|
role: Rôle
|
||||||
|
network: Réseau
|
||||||
|
battery: Batterie
|
||||||
|
bitrate: Débit
|
||||||
|
fps: FPS
|
||||||
|
last_seen: Dernier contact
|
||||||
table:
|
table:
|
||||||
type: Type
|
type: Type
|
||||||
when: Quand
|
when: Quand
|
||||||
meta: Méta
|
meta: Détails
|
||||||
links:
|
links:
|
||||||
watch_title: Lien en direct
|
watch_title: Lien en direct
|
||||||
none: Aucun lien vidéo disponible pour cette session.
|
none: Aucun lien vidéo disponible pour cette session.
|
||||||
|
|||||||
@@ -272,9 +272,28 @@ it:
|
|||||||
sessions:
|
sessions:
|
||||||
index:
|
index:
|
||||||
title: Sessioni di streaming
|
title: Sessioni di streaming
|
||||||
|
lead: Cerca per società, squadra, avversario o sede. Filtra per stato, piattaforma, nodo e periodo.
|
||||||
|
results: "Mostrate %{shown} di %{total}"
|
||||||
|
none: Nessuna sessione trovata con questi filtri.
|
||||||
|
filters:
|
||||||
|
q: Cerca
|
||||||
|
q_placeholder: Società, squadra, avversario…
|
||||||
|
status: Stato
|
||||||
|
platform: Piattaforma
|
||||||
|
club: Società
|
||||||
|
node: Nodo ingest
|
||||||
|
from: Da
|
||||||
|
to: A
|
||||||
|
any: Tutti
|
||||||
|
apply: Filtra
|
||||||
|
reset: Azzera
|
||||||
table:
|
table:
|
||||||
|
club: Società
|
||||||
match: Match
|
match: Match
|
||||||
status: Stato
|
status: Stato
|
||||||
|
started: Inizio
|
||||||
|
ended: Fine
|
||||||
|
duration: Durata
|
||||||
ingest: Ingest
|
ingest: Ingest
|
||||||
disconnects: Disconnessioni
|
disconnects: Disconnessioni
|
||||||
link: Link
|
link: Link
|
||||||
@@ -282,19 +301,59 @@ it:
|
|||||||
regia: Regia
|
regia: Regia
|
||||||
show:
|
show:
|
||||||
title: "Sessione %{id}"
|
title: "Sessione %{id}"
|
||||||
|
heading: Dettaglio sessione
|
||||||
|
back: "← Torna alle sessioni"
|
||||||
|
club_label: "Società:"
|
||||||
|
summary_title: Riepilogo
|
||||||
|
timing_title: Tempi
|
||||||
|
ingest_title: Ingest e media
|
||||||
|
devices_title: Dispositivi
|
||||||
|
events_title: Eventi
|
||||||
|
events_none: Nessun evento registrato.
|
||||||
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."
|
||||||
match_label: "Match: %{team} vs %{opponent}"
|
match_label: "Match: %{team} vs %{opponent}"
|
||||||
youtube_studio: YouTube Studio
|
youtube_studio: YouTube Studio
|
||||||
broadcast: Broadcast
|
broadcast: Broadcast
|
||||||
|
open_replays: Apri replay società
|
||||||
|
audio_muted: Audio muto
|
||||||
|
audio_on: Audio attivo
|
||||||
ingest_node: "Nodo ingest:"
|
ingest_node: "Nodo ingest:"
|
||||||
rtmp_ingest: "RTMP ingest:"
|
rtmp_ingest: "RTMP ingest:"
|
||||||
events_title: Eventi
|
fields:
|
||||||
|
id: ID
|
||||||
|
club: Società
|
||||||
|
team: Squadra
|
||||||
|
opponent: Avversario
|
||||||
|
operator: Operatore
|
||||||
|
platform: Piattaforma
|
||||||
|
privacy: Privacy
|
||||||
|
quality: Qualità
|
||||||
|
min_quality: Qualità minima
|
||||||
|
audio: Audio
|
||||||
|
created: Creata
|
||||||
|
started: Inizio
|
||||||
|
ended: Fine
|
||||||
|
duration: Durata
|
||||||
|
disconnects: Disconnessioni
|
||||||
|
scheduled: Partita programmata
|
||||||
|
location: Sede
|
||||||
|
node: Nodo
|
||||||
|
rtmp: RTMP ingest
|
||||||
|
hls: HLS
|
||||||
|
recording: Replay
|
||||||
|
devices:
|
||||||
|
role: Ruolo
|
||||||
|
network: Rete
|
||||||
|
battery: Batteria
|
||||||
|
bitrate: Bitrate
|
||||||
|
fps: FPS
|
||||||
|
last_seen: Ultimo contatto
|
||||||
table:
|
table:
|
||||||
type: Tipo
|
type: Tipo
|
||||||
when: Quando
|
when: Quando
|
||||||
meta: Meta
|
meta: Dettagli
|
||||||
links:
|
links:
|
||||||
watch_title: Link diretta
|
watch_title: Link diretta
|
||||||
none: Nessun link video disponibile per questa sessione.
|
none: Nessun link video disponibile per questa sessione.
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ Rails.application.routes.draw do
|
|||||||
post "signup", to: "registrations#create"
|
post "signup", to: "registrations#create"
|
||||||
get "login", to: "sessions#new"
|
get "login", to: "sessions#new"
|
||||||
post "login", to: "sessions#create"
|
post "login", to: "sessions#create"
|
||||||
delete "logout", to: "sessions#destroy"
|
match "logout", to: "sessions#destroy", via: %i[delete get]
|
||||||
patch "locale", to: "locales#update", as: :locale
|
patch "locale", to: "locales#update", as: :locale
|
||||||
get "password/forgot", to: "password_resets#new", as: :password_forgot
|
get "password/forgot", to: "password_resets#new", as: :password_forgot
|
||||||
post "password/forgot", to: "password_resets#create"
|
post "password/forgot", to: "password_resets#create"
|
||||||
|
|||||||
@@ -234,6 +234,8 @@ body.admin-body {
|
|||||||
.badge--ingest-home { background: #1565c0; color: #e3f2fd; }
|
.badge--ingest-home { background: #1565c0; color: #e3f2fd; }
|
||||||
.badge--ingest-lab { background: #6a1b9a; color: #f3e5f5; }
|
.badge--ingest-lab { background: #6a1b9a; color: #f3e5f5; }
|
||||||
.badge--ingest-cloud { background: #e65100; color: #fff3e0; }
|
.badge--ingest-cloud { background: #e65100; color: #fff3e0; }
|
||||||
|
.badge--ended { background: #37474f; color: #eceff1; }
|
||||||
|
.badge--error { background: #b71c1c; color: #ffebee; }
|
||||||
|
|
||||||
.admin-ingest {
|
.admin-ingest {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -743,3 +745,193 @@ body.admin-body {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-sessions-filters {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-filter-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-filter-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-filter-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.3rem;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--muted);
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-filter-field input,
|
||||||
|
.admin-filter-field select {
|
||||||
|
font: inherit;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text);
|
||||||
|
background: #0d0d12;
|
||||||
|
border: 1px solid var(--card-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.45rem 0.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-filter-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-filter-count {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table-strong {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table-sub {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
margin-top: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-session-head {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-session-head-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-session-match {
|
||||||
|
margin: 0.35rem 0 0;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-session-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.admin-session-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-session-grid .panel h3,
|
||||||
|
.panel > h3 {
|
||||||
|
margin: 0 0 0.85rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-kv {
|
||||||
|
margin: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-kv > div {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(110px, 34%) 1fr;
|
||||||
|
gap: 0.5rem 0.75rem;
|
||||||
|
align-items: start;
|
||||||
|
padding-bottom: 0.55rem;
|
||||||
|
border-bottom: 1px solid rgba(42, 42, 54, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-kv > div:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-kv dt {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-kv dd {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-mono {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-mono--wrap {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-event-type {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-event-meta {
|
||||||
|
margin: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-event-meta__row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(90px, 28%) 1fr;
|
||||||
|
gap: 0.35rem 0.6rem;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-event-meta dt {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-event-meta dd {
|
||||||
|
margin: 0;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-event-meta code {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: #d7d7e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table--events td {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-links-panel {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
require "rails_helper"
|
||||||
|
|
||||||
|
RSpec.describe "Admin sessions index", type: :request do
|
||||||
|
let!(:admin) { AdminAccount.create!(username: "ops-sessions", password: "Password123") }
|
||||||
|
let!(:coach) do
|
||||||
|
User.create!(email: "sessions-coach@test.it", name: "Coach", password: "Password123", role: "coach")
|
||||||
|
end
|
||||||
|
let!(:club_a) { Club.create!(name: "Tigers Club", sport: "volleyball") }
|
||||||
|
let!(:club_b) { Club.create!(name: "Other Club", sport: "volleyball") }
|
||||||
|
let!(:team_a) { club_a.teams.create!(name: "Tigers U16", sport_key: "pallavolo") }
|
||||||
|
let!(:team_b) { club_b.teams.create!(name: "Other U14", sport_key: "pallavolo") }
|
||||||
|
let!(:match_a) { team_a.matches.create!(opponent_name: "Rival A", sport_key: "pallavolo") }
|
||||||
|
let!(:match_b) { team_b.matches.create!(opponent_name: "Rival B", sport_key: "pallavolo") }
|
||||||
|
let!(:session_a) do
|
||||||
|
StreamSession.create!(
|
||||||
|
match: match_a,
|
||||||
|
user: coach,
|
||||||
|
platform: "matchlivetv",
|
||||||
|
status: "ended",
|
||||||
|
started_at: 2.hours.ago,
|
||||||
|
ended_at: 1.hour.ago,
|
||||||
|
total_duration_secs: 3600
|
||||||
|
)
|
||||||
|
end
|
||||||
|
let!(:session_b) do
|
||||||
|
StreamSession.create!(
|
||||||
|
match: match_b,
|
||||||
|
user: coach,
|
||||||
|
platform: "youtube",
|
||||||
|
status: "live",
|
||||||
|
started_at: 30.minutes.ago
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def login!
|
||||||
|
post admin_login_path, params: { username: admin.username, password: "Password123" }
|
||||||
|
end
|
||||||
|
|
||||||
|
before { login! }
|
||||||
|
|
||||||
|
it "mostra società, orari e filtra per testo" do
|
||||||
|
get admin_sessions_path, params: { q: "Tigers" }
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(response.body).to include("Tigers Club")
|
||||||
|
expect(response.body).to include("Tigers U16 vs Rival A")
|
||||||
|
expect(response.body).not_to include("Other U14 vs Rival B")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "filtra per stato" do
|
||||||
|
get admin_sessions_path, params: { status: "live" }
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(response.body).to include("Other U14 vs Rival B")
|
||||||
|
expect(response.body).not_to include("Tigers U16 vs Rival A")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "mostra il dettaglio formattato" do
|
||||||
|
get admin_session_path(session_a)
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(response.body).to include("Tigers Club")
|
||||||
|
expect(response.body).to include("Dettaglio sessione").or include("Session details")
|
||||||
|
expect(response.body).to include(session_a.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
require "rails_helper"
|
||||||
|
|
||||||
|
RSpec.describe "Public logout", type: :request do
|
||||||
|
let!(:user) do
|
||||||
|
User.create!(email: "logout-user@test.it", name: "Logout", password: "Password123", role: "coach")
|
||||||
|
end
|
||||||
|
|
||||||
|
def login!
|
||||||
|
ActionController::Base.allow_forgery_protection = false
|
||||||
|
post public_login_path, params: { email: user.email, password: "Password123" }
|
||||||
|
ActionController::Base.allow_forgery_protection = true
|
||||||
|
end
|
||||||
|
|
||||||
|
around do |example|
|
||||||
|
was = ActionController::Base.allow_forgery_protection
|
||||||
|
example.run
|
||||||
|
ensure
|
||||||
|
ActionController::Base.allow_forgery_protection = was
|
||||||
|
end
|
||||||
|
|
||||||
|
it "disconnette anche senza authenticity_token (CSRF stale)" do
|
||||||
|
login!
|
||||||
|
expect(session[:user_id]).to eq(user.id)
|
||||||
|
|
||||||
|
delete "/logout"
|
||||||
|
expect(response).to redirect_to(public_pricing_path)
|
||||||
|
follow_redirect!
|
||||||
|
expect(session[:user_id]).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "accetta anche GET /logout come fallback" do
|
||||||
|
login!
|
||||||
|
get "/logout"
|
||||||
|
expect(response).to redirect_to(public_pricing_path)
|
||||||
|
expect(session[:user_id]).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -22,6 +22,7 @@ if ssh "$SERVER" 'command -v rsync >/dev/null 2>&1'; then
|
|||||||
--exclude 'backend/tmp' \
|
--exclude 'backend/tmp' \
|
||||||
--exclude 'backend/.bundle' \
|
--exclude 'backend/.bundle' \
|
||||||
--exclude 'infra/.env' \
|
--exclude 'infra/.env' \
|
||||||
|
--exclude 'infra/slates/custom/' \
|
||||||
--exclude 'infra/garage/garage.prod.toml' \
|
--exclude 'infra/garage/garage.prod.toml' \
|
||||||
--exclude 'infra/garage/prod-credentials.env' \
|
--exclude 'infra/garage/prod-credentials.env' \
|
||||||
--exclude 'log/' \
|
--exclude 'log/' \
|
||||||
|
|||||||
Reference in New Issue
Block a user