diff --git a/backend/app/controllers/api/v1/recordings_controller.rb b/backend/app/controllers/api/v1/recordings_controller.rb index b10e995..f673978 100644 --- a/backend/app/controllers/api/v1/recordings_controller.rb +++ b/backend/app/controllers/api/v1/recordings_controller.rb @@ -5,7 +5,10 @@ module Api def update authorize_manage! - @recording.update!(recording_update_attrs) + attrs = recording_update_attrs + privacy_changed = attrs.key?(:privacy_status) && attrs[:privacy_status] != @recording.privacy_status + @recording.update!(attrs) + Recordings::SyncYoutubePrivacyJob.perform_async(@recording.id) if privacy_changed && @recording.youtube_video_id.present? render json: recording_json(@recording) end @@ -96,7 +99,10 @@ module Api youtube_video_id: recording.youtube_video_id, youtube_watch_url: recording.youtube_watch_url, youtube_publish_enabled: ent.premium_full? && ent.youtube_enabled?, + source_platform: recording.source_platform, + source_platform_label: recording.source_platform_label, expires_at: recording.expires_at, + days_until_expiry: recording.days_until_expiry, metadata: recording.metadata } end diff --git a/backend/app/controllers/api/v1/teams_controller.rb b/backend/app/controllers/api/v1/teams_controller.rb index f1fab7d..f3b8b4d 100644 --- a/backend/app/controllers/api/v1/teams_controller.rb +++ b/backend/app/controllers/api/v1/teams_controller.rb @@ -42,6 +42,7 @@ module Api recs = team.recordings.not_deleted .where(status: %w[ready processing]) + .where("expires_at IS NULL OR expires_at > ?", Time.current) .includes(stream_session: :match) .order(recorded_at: :desc, created_at: :desc) .limit(50) @@ -167,7 +168,10 @@ module Api youtube_video_id: recording.youtube_video_id, youtube_watch_url: recording.youtube_watch_url, youtube_publish_enabled: ent.premium_full? && ent.youtube_enabled?, + source_platform: recording.source_platform, + source_platform_label: recording.source_platform_label, expires_at: recording.expires_at, + days_until_expiry: recording.days_until_expiry, metadata: recording.metadata } end diff --git a/backend/app/controllers/public/club_recordings_controller.rb b/backend/app/controllers/public/club_recordings_controller.rb index 07dc7d0..3d4f770 100644 --- a/backend/app/controllers/public/club_recordings_controller.rb +++ b/backend/app/controllers/public/club_recordings_controller.rb @@ -2,7 +2,7 @@ module Public class ClubRecordingsController < WebBaseController before_action :require_login! before_action :set_club - before_action -> { require_club_owner!(@club) } + before_action :require_club_recording_manager! before_action :set_recording, only: %i[update destroy publish_youtube] def index @@ -11,17 +11,39 @@ module Public .not_deleted .includes(stream_session: { match: :team }) .order(recorded_at: :desc, created_at: :desc) + if params[:team_id].present? + @recordings = @recordings.where(team_id: params[:team_id]) + end + if params[:status].present? + case params[:status] + when "ready" + @recordings = @recordings.ready + when "processing" + @recordings = @recordings.where(status: "processing") + when "expired" + @recordings = @recordings.where( + "recordings.status IN (?) OR (recordings.expires_at IS NOT NULL AND recordings.expires_at <= ?)", + %w[expired failed], Time.current + ) + end + end @teams = @club.teams.order(:name) + @filter_team_id = params[:team_id] + @filter_status = params[:status] end def update - privacy = params.require(:recording).permit(:privacy_status, :title)[:privacy_status] + permitted = params.require(:recording).permit(:privacy_status, :title) attrs = {} + privacy = permitted[:privacy_status] attrs[:privacy_status] = privacy if privacy.in?(Recording::PRIVACY_STATUSES) - title = params.dig(:recording, :title) + title = permitted[:title] attrs[:title] = title if title.present? + privacy_changed = attrs.key?(:privacy_status) && attrs[:privacy_status] != @recording.privacy_status @recording.update!(attrs) - redirect_to public_club_recordings_path(@club), notice: "Replay aggiornato" + Recordings::SyncYoutubePrivacyJob.perform_async(@recording.id) if privacy_changed && @recording.youtube_video_id.present? + redirect_to public_club_recordings_path(@club, team_id: params[:team_id], status: params[:status]), + notice: "Replay aggiornato" rescue ActiveRecord::RecordInvalid => e redirect_to public_club_recordings_path(@club), alert: e.record.errors.full_messages.join(", ") end @@ -45,11 +67,21 @@ module Public private def set_club - @club = current_user.owned_clubs.find(params[:club_id]) + @club = Club.find(params[:club_id]) + end + + def require_club_recording_manager! + return if current_user.owned_clubs.exists?(id: @club.id) + return if current_user.manageable_teams.joins(:club).exists?(clubs: { id: @club.id }) + + redirect_to public_clubs_path, alert: "Non autorizzato" end def set_recording @recording = Recording.for_club(@club).not_deleted.find(params[:id]) + return if Recordings::AccessPolicy.new(@recording, viewer: current_user).manage_allowed? + + raise ActiveRecord::RecordNotFound end end end diff --git a/backend/app/jobs/recordings/sync_youtube_privacy_job.rb b/backend/app/jobs/recordings/sync_youtube_privacy_job.rb new file mode 100644 index 0000000..8673ba1 --- /dev/null +++ b/backend/app/jobs/recordings/sync_youtube_privacy_job.rb @@ -0,0 +1,17 @@ +module Recordings + class SyncYoutubePrivacyJob + include Sidekiq::Job + sidekiq_options retry: 2, queue: "default" + + def perform(recording_id) + recording = Recording.find_by(id: recording_id) + return unless recording&.youtube_video_id.present? + + Youtube::VideoLifecycleService.new(recording).update_privacy!( + privacy_status: recording.privacy_status + ) + rescue Youtube::VideoLifecycleService::Error => e + Rails.logger.warn("[SyncYoutubePrivacyJob] #{recording_id}: #{e.message}") + end + end +end diff --git a/backend/app/models/recording.rb b/backend/app/models/recording.rb index 51dec84..7ba9483 100644 --- a/backend/app/models/recording.rb +++ b/backend/app/models/recording.rb @@ -141,6 +141,28 @@ class Recording < ApplicationRecord count == 1 ? "1 visualizzazione" : "#{count} visualizzazioni" end + def source_platform + metadata.is_a?(Hash) ? metadata["source_platform"].presence : nil + end + + def source_platform_label + case source_platform + when "youtube" then "YouTube" + when "matchlivetv" then "Match Live TV" + else "—" + end + end + + def playable_on_site? + ready? && storage_key.present? + end + + def days_until_expiry + return nil unless expires_at + + ((expires_at - Time.current) / 1.day).ceil + end + def status_label return "Eliminato" if deleted? return "Scaduto" if status == "expired" diff --git a/backend/app/services/recordings/delete.rb b/backend/app/services/recordings/delete.rb index d01c6a9..e31a4db 100644 --- a/backend/app/services/recordings/delete.rb +++ b/backend/app/services/recordings/delete.rb @@ -6,6 +6,7 @@ module Recordings end def call + delete_youtube_video delete_storage_object if @recording.storage_key.present? cleanup_local_artifacts @@ -13,13 +14,23 @@ module Recordings status: "expired", deleted_at: Time.current, storage_key: nil, - thumbnail_storage_key: nil + thumbnail_storage_key: nil, + youtube_video_id: nil, + youtube_published_at: nil ) @recording end private + def delete_youtube_video + return if @recording.youtube_video_id.blank? + + Youtube::VideoLifecycleService.new(@recording).delete! + rescue Youtube::VideoLifecycleService::Error => e + Rails.logger.warn("[Recordings::Delete] YouTube delete #{@recording.id}: #{e.message}") + end + def delete_storage_object storage = Recordings::Storage.new storage.delete(key: @recording.storage_key) if @recording.storage_key.present? diff --git a/backend/app/services/recordings/finalize_session.rb b/backend/app/services/recordings/finalize_session.rb index 826435d..2e7bb7a 100644 --- a/backend/app/services/recordings/finalize_session.rb +++ b/backend/app/services/recordings/finalize_session.rb @@ -6,7 +6,7 @@ module Recordings def call team = @session.match.team - return unless team.entitlements.can_access_recordings? + return unless team.entitlements.can_create_recordings? retention_days = team.entitlements.recording_retention_days expires_at = retention_days.positive? ? retention_days.days.from_now : nil diff --git a/backend/app/services/recordings/serve_from_storage.rb b/backend/app/services/recordings/serve_from_storage.rb index 3fd179c..bccb2c5 100644 --- a/backend/app/services/recordings/serve_from_storage.rb +++ b/backend/app/services/recordings/serve_from_storage.rb @@ -5,6 +5,8 @@ require "aws-sdk-s3" module Recordings # Serve file replay/thumbnail al browser senza esporre URL S3 interni (es. http://garage:3900). class ServeFromStorage + CHUNK_SIZE = 256 * 1024 + def initialize(key:, content_type:, disposition: "inline", filename: nil) @key = key @content_type = content_type @@ -14,7 +16,15 @@ module Recordings def call(controller) path = local_path - return controller.send_file(path, type: @content_type, disposition: @disposition, filename: @filename) if path + if path + return controller.send_file( + path, + type: @content_type, + disposition: @disposition, + filename: @filename, + stream: true + ) + end proxy_s3(controller) end @@ -41,22 +51,24 @@ module Recordings controller.response.headers["Accept-Ranges"] = "bytes" controller.response.headers["Content-Length"] = resp.content_length.to_s if resp.content_length controller.response.headers["Content-Range"] = resp.content_range if resp.content_range.present? + controller.response.headers["Content-Disposition"] = content_disposition_header - data = resp.body.read - max = 1.gigabyte - raise ArgumentError, "File troppo grande per il proxy" if data.bytesize > max - - if @disposition == "attachment" && @filename.present? - controller.send_data( - data, - type: @content_type, - disposition: "attachment", - filename: @filename - ) - else - controller.send_data(data, type: @content_type, disposition: "inline") - end controller.status = :partial_content if range.present? && resp.content_range.present? + + body = resp.body + controller.response_body = Enumerator.new do |yielder| + while (chunk = body.read(CHUNK_SIZE)) + yielder << chunk + end + end + end + + def content_disposition_header + if @disposition == "attachment" && @filename.present? + %(attachment; filename="#{@filename.gsub('"', '\\"')}") + else + "inline" + end end def s3_client @@ -66,7 +78,6 @@ module Recordings endpoint: MatchLiveTv.replay_storage_endpoint, region: MatchLiveTv.replay_storage_region, force_path_style: MatchLiveTv.replay_storage_force_path_style?, - # Garage non sempre allinea i checksum AWS su Range/read parziali response_checksum_validation: "WHEN_REQUIRED" ) end diff --git a/backend/app/services/teams/entitlements.rb b/backend/app/services/teams/entitlements.rb index 41b2c61..b656672 100644 --- a/backend/app/services/teams/entitlements.rb +++ b/backend/app/services/teams/entitlements.rb @@ -178,12 +178,18 @@ module Teams assert_can_stream_on!("youtube") end - def can_access_recordings? + # Nuove registrazioni (MediaMTX + FinalizeSession): richiede abbonamento attivo. + def can_create_recordings? active? && plan.recordings_enabled? end + # Archivio esistente: visibile fino a expires_at anche se l'abbonamento è scaduto. + def can_access_recordings? + plan.recordings_enabled? || @team.recordings.not_deleted.exists? + end + def recording_enabled_for_mediamtx? - can_access_recordings? + can_create_recordings? end def recording_retention_days @@ -215,6 +221,7 @@ module Teams concurrent_streams_used: concurrent_streams_used, concurrent_streams_limit: concurrent_streams_limit, recordings_enabled: can_access_recordings?, + can_create_recordings: can_create_recordings?, recording_retention_days: recording_retention_days, phone_download_enabled: phone_download_enabled?, youtube_enabled: youtube_enabled?, diff --git a/backend/app/services/youtube/video_lifecycle_service.rb b/backend/app/services/youtube/video_lifecycle_service.rb new file mode 100644 index 0000000..a926851 --- /dev/null +++ b/backend/app/services/youtube/video_lifecycle_service.rb @@ -0,0 +1,66 @@ +module Youtube + # Aggiorna privacy o elimina un VOD YouTube collegato a un replay. + class VideoLifecycleService + class Error < StandardError; end + + def initialize(recording) + @recording = recording + @team = recording.team + @session = recording.stream_session + end + + def update_privacy!(privacy_status:) + video_id = @recording.youtube_video_id + return if video_id.blank? || video_id.to_s.start_with?("mock_") + + privacy = map_privacy(privacy_status) + client = authorized_client + video = Google::Apis::YoutubeV3::Video.new( + id: video_id, + status: Google::Apis::YoutubeV3::VideoStatus.new( + privacy_status: privacy, + self_declared_made_for_kids: false + ) + ) + client.update_video("status", video) + merge_youtube_meta!("privacy_status" => privacy) + true + rescue Google::Apis::Error => e + raise Error, e.message + end + + def delete! + video_id = @recording.youtube_video_id + return if video_id.blank? || video_id.to_s.start_with?("mock_") + + client = authorized_client + client.delete_video(video_id) + true + rescue Google::Apis::Error => e + raise Error, e.message + end + + private + + def map_privacy(status) + status.to_s == "public" ? "public" : "unlisted" + end + + def merge_youtube_meta!(attrs) + meta = @recording.metadata.is_a?(Hash) ? @recording.metadata.dup : {} + publish = meta.fetch("youtube_publish", {}).merge(attrs) + @recording.update!(metadata: meta.merge("youtube_publish" => publish)) + end + + def youtube_channel + @session&.platform == "youtube" ? "team" : nil + end + + def authorized_client + credential = CredentialResolver.new(@team, channel: youtube_channel).resolve + raise Error, "Credenziali YouTube non disponibili" if credential.blank? + + OauthRefresh.new(credential).apply!(Google::Apis::YoutubeV3::YouTubeService.new) + end + end +end diff --git a/backend/app/views/public/club_recordings/index.html.erb b/backend/app/views/public/club_recordings/index.html.erb index 934d53d..6ab75f4 100644 --- a/backend/app/views/public/club_recordings/index.html.erb +++ b/backend/app/views/public/club_recordings/index.html.erb @@ -5,7 +5,11 @@ <%= link_to "← #{@club.name}", public_club_path(@club), class: "back-link" %>

Archivio Replay

-

Gestisci registrazioni, visibilità, download e pubblicazione YouTube.

+

+ Gestisci registrazioni, visibilità e download. + Privato = link non indicizzato, non compare in /replay pubblico. + Eliminando un replay vengono rimossi anche i file sul server e il video YouTube collegato. +

@@ -26,6 +30,23 @@
+ <%= form_with url: public_club_recordings_path(@club), method: :get, local: true, class: "replay-filter-form" do %> + + + + + <% end %> + <% if @recordings.any? %>
@@ -33,6 +54,7 @@ + @@ -50,22 +72,40 @@ <% end %> - + + - + <% end %> diff --git a/backend/app/views/public/replay/index.html.erb b/backend/app/views/public/replay/index.html.erb index 36d6a32..ebc7e31 100644 --- a/backend/app/views/public/replay/index.html.erb +++ b/backend/app/views/public/replay/index.html.erb @@ -67,6 +67,9 @@ <%= rec.title_or_default %>

<%= match.team.club.name %> · <%= l_local(rec.recorded_at_or_fallback) %> + <% if rec.source_platform_label != "—" %> + · <%= rec.source_platform_label %> + <% end %>

<%= rec.views_label %> diff --git a/backend/app/views/public/replay/show.html.erb b/backend/app/views/public/replay/show.html.erb index e5a471e..9c16c90 100644 --- a/backend/app/views/public/replay/show.html.erb +++ b/backend/app/views/public/replay/show.html.erb @@ -10,6 +10,9 @@

<%= @match.team.club.name %> · <%= l_local(@recording.recorded_at_or_fallback) %> · <%= @recording.views_label %> + <% if @recording.source_platform_label != "—" %> + · <%= @recording.source_platform_label %> + <% end %>

@@ -46,6 +49,22 @@ <% end %> + <% elsif @recording.ready? && @recording.youtube_watch_url.present? %> +
+
+ +
+
+
+

+ Replay disponibile su YouTube (copia server non presente). + <%= link_to "Apri su YouTube", @recording.youtube_watch_url, target: "_blank", rel: "noopener" %> +

+
<% elsif @recording.ready? %>

Il file video non è più disponibile (archivio rimosso o in migrazione).

diff --git a/backend/spec/services/recordings/delete_spec.rb b/backend/spec/services/recordings/delete_spec.rb new file mode 100644 index 0000000..52907ea --- /dev/null +++ b/backend/spec/services/recordings/delete_spec.rb @@ -0,0 +1,38 @@ +require "rails_helper" + +RSpec.describe Recordings::Delete do + let!(:club) { Club.create!(name: "Club", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff") } + let!(:team) { club.teams.create!(name: "Team", sport: "volleyball") } + let!(:user) { User.create!(email: "coach@test.com", name: "Coach", password: "password123", role: "coach") } + let!(:match) { team.matches.create!(opponent_name: "Rival", scheduled_at: 1.day.from_now) } + let!(:session) do + StreamSession.create!(match: match, user: user, platform: "youtube", status: "ended") + end + let!(:recording) do + Recording.create!( + stream_session: session, + team: team, + status: "ready", + privacy_status: "public", + expires_at: 10.days.from_now, + storage_key: "teams/#{team.id}/sessions/#{session.id}/replay.mp4", + youtube_video_id: "abc123xyz" + ) + end + + it "elimina storage e revoca youtube_video_id" do + storage = instance_double(Recordings::Storage, delete: true) + allow(Recordings::Storage).to receive(:new).and_return(storage) + yt = instance_double(Youtube::VideoLifecycleService, delete!: true) + allow(Youtube::VideoLifecycleService).to receive(:new).with(recording).and_return(yt) + + described_class.new(recording).call + + expect(yt).to have_received(:delete!) + expect(storage).to have_received(:delete).at_least(:once) + recording.reload + expect(recording.deleted_at).to be_present + expect(recording.youtube_video_id).to be_nil + expect(recording.storage_key).to be_nil + end +end diff --git a/backend/spec/services/recordings/finalize_session_spec.rb b/backend/spec/services/recordings/finalize_session_spec.rb index 6932757..f13ce94 100644 --- a/backend/spec/services/recordings/finalize_session_spec.rb +++ b/backend/spec/services/recordings/finalize_session_spec.rb @@ -32,4 +32,16 @@ RSpec.describe Recordings::FinalizeSession do Billing::AssignPlan.call(club: club, plan_slug: "free") expect(described_class.new(session).call).to be_nil end + + it "imposta retention 90 giorni con premium full" do + Billing::AssignPlan.call(club: club, plan_slug: "premium_full") + rec = described_class.new(session).call + expect(rec.expires_at).to be > 89.days.from_now + end + + it "non crea recording se abbonamento scaduto" do + Billing::AssignPlan.call(club: club, plan_slug: "premium_light") + club.subscription.update!(status: "canceled") + expect(described_class.new(session).call).to be_nil + end end diff --git a/backend/spec/services/teams/entitlements_recordings_spec.rb b/backend/spec/services/teams/entitlements_recordings_spec.rb new file mode 100644 index 0000000..09661f6 --- /dev/null +++ b/backend/spec/services/teams/entitlements_recordings_spec.rb @@ -0,0 +1,41 @@ +require "rails_helper" + +RSpec.describe Teams::Entitlements, "recordings" do + let!(:club) { Club.create!(name: "Club", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff") } + let!(:team) { club.teams.create!(name: "Team", sport: "volleyball") } + let!(:user) { User.create!(email: "coach@test.com", name: "Coach", password: "password123", role: "coach") } + let!(:match) { team.matches.create!(opponent_name: "Rival", scheduled_at: 1.day.from_now) } + let!(:session) do + StreamSession.create!(match: match, user: user, platform: "matchlivetv", status: "ended") + end + + before { load Rails.root.join("db/seeds/plans.rb") } + + it "premium attivo può creare registrazioni" do + Billing::AssignPlan.call(club: club, plan_slug: "premium_light") + ent = described_class.new(team) + expect(ent.can_create_recordings?).to be true + expect(ent.can_access_recordings?).to be true + end + + it "abbonamento scaduto non crea ma accede all'archivio esistente" do + Billing::AssignPlan.call(club: club, plan_slug: "premium_light") + club.subscription.update!(status: "canceled") + Recording.create!( + stream_session: session, + team: team, + status: "ready", + privacy_status: "unlisted", + expires_at: 5.days.from_now, + storage_key: "k" + ) + ent = described_class.new(team.reload) + expect(ent.can_create_recordings?).to be false + expect(ent.can_access_recordings?).to be true + end + + it "premium full ha retention 90 giorni" do + Billing::AssignPlan.call(club: club, plan_slug: "premium_full") + expect(described_class.new(team).recording_retention_days).to eq(90) + end +end diff --git a/docs/REPLAY_MODULE.md b/docs/REPLAY_MODULE.md index a14f12c..7b6f4cb 100644 --- a/docs/REPLAY_MODULE.md +++ b/docs/REPLAY_MODULE.md @@ -32,10 +32,15 @@ MediaMTX registra **solo mentre il telefono pubblica** (RTMP connesso). In pausa - Contatore visualizzazioni su ogni play (`view_count`) ### 5. Visibilità -Pubblico / Privato (unlisted), modificabile dall’admin società. +- **Pubblico** (`public`): compare in `/replay`, indicizzabile, YouTube `public` +- **Privato** (`unlisted`): non in catalogo pubblico, accessibile solo con link diretto, `noindex`, YouTube `unlisted` + +Modificabile dall’archivio società (`/clubs/:id/replays`). Il cambio privacy sincronizza automaticamente il VOD YouTube se presente (`Recordings::SyncYoutubePrivacyJob`). ### 6. Eliminazione anticipata -`Recordings::Delete` — DB + video + thumbnail su storage. +`Recordings::Delete` — elimina video YouTube collegato, oggetti S3/Garage (MP4 + thumbnail) e soft-delete DB. + +Stesso comportamento alla scadenza retention (`PurgeExpiredJob` alle 03:00). ### 7. Dashboard KPI Replay disponibili, in scadenza (7 gg), spazio occupato, **visualizzazioni totali**. @@ -115,6 +120,22 @@ Per ridimensionare dopo: bash scripts/garage_set_capacity.sh 85G # oppure senza argomento: calcolo automatico ``` +## Piattaforme live e replay + +| Origine diretta | Copia Garage/S3 | Player sito `/replay/:id` | YouTube VOD | +|-----------------|-----------------|----------------------------|-------------| +| `matchlivetv` | Sì (Premium) | MP4 via proxy Rails | Manuale (Premium Full) | +| `youtube` | Sì (Premium) | MP4 via proxy Rails | Auto se Premium Full + diretta YouTube | + +Se il file S3 non è disponibile ma esiste `youtube_video_id`, la pagina replay mostra embed YouTube. + +## Retention e abbonamento + +- `expires_at` viene impostato alla fine diretta: **30 giorni** (Premium Light), **90 giorni** (Premium Full) +- `expires_at` **non cambia** al cambio piano successivo +- **Nuove registrazioni** richiedono abbonamento attivo (`can_create_recordings?`) +- **Archivio esistente** resta accessibile fino a `expires_at` anche se l’abbonamento scade (`can_access_recordings?`) + ## API mobile - `GET /teams/:id/recordings` — lista con thumbnail, views, download_enabled
TitoloPiattaforma Data Durata Views<%= rec.title_or_default %> + <%= form_with url: public_club_recording_path(@club, rec), method: :patch, local: true do %> + <%= hidden_field_tag :team_id, @filter_team_id if @filter_team_id.present? %> + <%= hidden_field_tag :status, @filter_status if @filter_status.present? %> + <%= text_field_tag "recording[title]", rec.title_or_default, class: "input", style: "min-width:12rem", onchange: "this.form.submit()" %> + <% end %> + <%= rec.source_platform_label %> <%= l_local(rec.recorded_at_or_fallback) %> <%= rec.duration_label %> <%= rec.view_count %><%= rec.expires_at ? l_local(rec.expires_at) : "—" %> + <% if rec.expires_at %> + <%= l_local(rec.expires_at) %> + <% if rec.days_until_expiry && rec.days_until_expiry.positive? %> +
(<%= rec.days_until_expiry %> gg) + <% end %> + <% else %> + — + <% end %> +
<%= rec.status_label %> <%= form_with url: public_club_recording_path(@club, rec), method: :patch, local: true do %> + <%= hidden_field_tag :team_id, @filter_team_id if @filter_team_id.present? %> + <%= hidden_field_tag :status, @filter_status if @filter_status.present? %> <%= select_tag "recording[privacy_status]", - options_for_select([["Pubblico", "public"], ["Privato", "unlisted"]], rec.privacy_status), + options_for_select([["Pubblico", "public"], ["Privato (link)", "unlisted"]], rec.privacy_status), onchange: "this.form.submit()" %> <% end %> <% if rec.replay_url %> - <%= link_to "Guarda", rec.replay_url, target: "_blank", rel: "noopener" %> + <%= link_to "Sito", rec.replay_url, target: "_blank", rel: "noopener" %> <% end %> <% if rec.team.entitlements.phone_download_enabled? && rec.ready? %> · <%= link_to "MP4", public_replay_download_path(rec.stream_session_id) %> @@ -77,8 +117,9 @@ · <%= link_to "YT", rec.youtube_watch_url, target: "_blank", rel: "noopener" %> <% end %> · <%= button_to "Elimina", public_club_recording_path(@club, rec), method: :delete, + params: { team_id: @filter_team_id, status: @filter_status }.compact, class: "btn-link", style: "display:inline;padding:0;border:0;background:none;color:#e53935;cursor:pointer", - form: { data: { turbo_confirm: "Eliminare definitivamente questo replay?" } } %> + form: { data: { turbo_confirm: "Eliminare definitivamente questo replay? Verranno rimossi i file sul server e il video YouTube collegato." } } %>