diff --git a/backend/app/controllers/concerns/recordings/club_archive_actions.rb b/backend/app/controllers/concerns/recordings/club_archive_actions.rb
index fddd316..2f52ca2 100644
--- a/backend/app/controllers/concerns/recordings/club_archive_actions.rb
+++ b/backend/app/controllers/concerns/recordings/club_archive_actions.rb
@@ -13,7 +13,12 @@ module Recordings
end
def update
- permitted = params.require(:recording).permit(:privacy_status, :title)
+ if admin_expiry_update_requested?
+ apply_admin_expiry_update!
+ return
+ end
+
+ permitted = recording_update_params
attrs = {}
privacy = permitted[:privacy_status]
attrs[:privacy_status] = privacy if privacy.in?(Recording::PRIVACY_STATUSES)
@@ -25,6 +30,8 @@ module Recordings
redirect_to archive_index_path, notice: "Replay aggiornato"
rescue ActiveRecord::RecordInvalid => e
redirect_to archive_index_path, alert: e.record.errors.full_messages.join(", ")
+ rescue Recordings::ExtendExpiry::Error => e
+ redirect_to archive_index_path, alert: e.message
end
def destroy
@@ -87,5 +94,30 @@ module Recordings
def archive_namespace
raise NotImplementedError
end
+
+ def recording_update_params
+ params.require(:recording).permit(:privacy_status, :title, :expires_at, :extend_days)
+ end
+
+ def admin_expiry_update_requested?
+ return false unless archive_namespace == :admin
+
+ permitted = recording_update_params
+ permitted[:extend_days].present? || permitted[:expires_at].present?
+ end
+
+ def apply_admin_expiry_update!
+ permitted = recording_update_params
+ extender = Recordings::ExtendExpiry.new(@recording)
+
+ if permitted[:extend_days].present?
+ extender.by_days(permitted[:extend_days])
+ redirect_to archive_index_path, notice: "Scadenza replay prolungata di #{permitted[:extend_days].to_i} giorni"
+ else
+ expires_at = Time.zone.parse(permitted[:expires_at].to_s)&.end_of_day
+ extender.to_time(expires_at)
+ redirect_to archive_index_path, notice: "Scadenza replay aggiornata"
+ end
+ end
end
end
diff --git a/backend/app/services/recordings/extend_expiry.rb b/backend/app/services/recordings/extend_expiry.rb
new file mode 100644
index 0000000..30d61d4
--- /dev/null
+++ b/backend/app/services/recordings/extend_expiry.rb
@@ -0,0 +1,39 @@
+module Recordings
+ class ExtendExpiry
+ class Error < StandardError; end
+
+ def initialize(recording)
+ @recording = recording
+ end
+
+ def to_time(expires_at)
+ raise Error, "Data scadenza obbligatoria" if expires_at.blank?
+
+ parsed = expires_at.is_a?(Time) || expires_at.is_a?(ActiveSupport::TimeWithZone) ? expires_at : Time.zone.parse(expires_at.to_s)
+ raise Error, "Data non valida" unless parsed
+
+ apply(parsed)
+ end
+
+ def by_days(days)
+ count = days.to_i
+ raise Error, "Giorni non validi" if count <= 0
+
+ base = [@recording.expires_at, Time.current].compact.max
+ apply(base + count.days)
+ end
+
+ private
+
+ def apply(new_expires_at)
+ raise Error, "Replay già eliminato" if @recording.deleted?
+ raise Error, "La scadenza deve essere nel futuro" unless new_expires_at > Time.current
+
+ @recording.update!(
+ expires_at: new_expires_at,
+ expiry_warning_sent_at: nil
+ )
+ @recording
+ end
+ end
+end
diff --git a/backend/app/views/recordings/_club_archive.html.erb b/backend/app/views/recordings/_club_archive.html.erb
index 9e613fc..cab1b4f 100644
--- a/backend/app/views/recordings/_club_archive.html.erb
+++ b/backend/app/views/recordings/_club_archive.html.erb
@@ -112,6 +112,26 @@
Nessuna scadenza
<% end %>
+ <% if local_assigns[:admin_mode] && !rec.deleted? && rec.expires_at.present? %>
+ <%= form_with url: paths.update.call(rec), method: :patch, local: true, class: "replay-archive__expiry-form" do %>
+ <% filter_params.each { |key, value| concat hidden_field_tag(key, value) } %>
+ <%= date_field_tag "recording[expires_at]",
+ rec.expires_at.to_date,
+ min: Date.tomorrow,
+ class: "replay-archive__expiry-input",
+ title: "Nuova data di scadenza" %>
+ <%= submit_tag "Salva", class: "replay-archive__expiry-save" %>
+ <% end %>
+
+ <% [[30, "+30 gg"], [90, "+90 gg"], [180, "+180 gg"]].each do |days, label| %>
+ <%= button_to label, paths.update.call(rec),
+ method: :patch,
+ params: filter_params.merge(recording: { extend_days: days }),
+ class: "replay-archive__expiry-extend",
+ title: "Prolunga la scadenza di #{days} giorni" %>
+ <% end %>
+
+ <% end %>
<%= rec.status_label %>
diff --git a/backend/public/marketing.css b/backend/public/marketing.css
index 9612bee..a38edc9 100644
--- a/backend/public/marketing.css
+++ b/backend/public/marketing.css
@@ -1593,6 +1593,55 @@ a.replay-archive__thumb:hover {
margin: 0;
}
+.replay-archive__expiry-form {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 6px;
+ margin-top: 8px;
+}
+
+.replay-archive__expiry-input {
+ font-size: 0.8rem;
+ padding: 4px 6px;
+ border-radius: 4px;
+ border: 1px solid #444;
+ background: #1a1a1a;
+ color: #eee;
+}
+
+.replay-archive__expiry-save {
+ font-size: 0.75rem;
+ padding: 4px 8px;
+ border-radius: 4px;
+ border: 1px solid #555;
+ background: #2a2a2a;
+ color: #ddd;
+ cursor: pointer;
+}
+
+.replay-archive__expiry-quick {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ margin-top: 6px;
+}
+
+.replay-archive__expiry-extend {
+ font-size: 0.7rem;
+ padding: 2px 6px;
+ border-radius: 4px;
+ border: 1px solid #444;
+ background: #222;
+ color: #aaa;
+ cursor: pointer;
+}
+
+.replay-archive__expiry-extend:hover {
+ color: #fff;
+ border-color: #666;
+}
+
.visually-hidden {
position: absolute;
width: 1px;
diff --git a/backend/spec/requests/admin/club_recordings_spec.rb b/backend/spec/requests/admin/club_recordings_spec.rb
index 4865121..cd9b18e 100644
--- a/backend/spec/requests/admin/club_recordings_spec.rb
+++ b/backend/spec/requests/admin/club_recordings_spec.rb
@@ -47,4 +47,29 @@ RSpec.describe "Admin club replays", type: :request do
expect(recording.reload.status).to eq("expired")
expect(recording.deleted_at).to be_present
end
+
+ it "prolunga la scadenza con giorni aggiuntivi" do
+ original = recording.expires_at
+ recording.update!(expires_at: 1.hour.ago, expiry_warning_sent_at: Time.current)
+
+ patch admin_club_recording_path(club, recording), params: { recording: { extend_days: 30 } }
+
+ expect(response).to redirect_to(admin_club_recordings_path(club))
+ recording.reload
+ expect(recording.expires_at).to be > Time.current
+ expect(recording.expires_at).to be_within(2.seconds).of(30.days.from_now)
+ expect(recording.expiry_warning_sent_at).to be_nil
+ expect(Recording.expired_pending_purge).not_to include(recording)
+ end
+
+ it "imposta una nuova data di scadenza" do
+ new_date = 60.days.from_now.to_date
+
+ patch admin_club_recording_path(club, recording), params: { recording: { expires_at: new_date } }
+
+ expect(response).to redirect_to(admin_club_recordings_path(club))
+ recording.reload
+ expect(recording.expires_at.to_date).to eq(new_date)
+ expect(recording.expiry_warning_sent_at).to be_nil
+ end
end
diff --git a/backend/spec/services/recordings/extend_expiry_spec.rb b/backend/spec/services/recordings/extend_expiry_spec.rb
new file mode 100644
index 0000000..7c5e6e6
--- /dev/null
+++ b/backend/spec/services/recordings/extend_expiry_spec.rb
@@ -0,0 +1,64 @@
+require "rails_helper"
+
+RSpec.describe Recordings::ExtendExpiry do
+ let!(:club) { Club.create!(name: "Club", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff") }
+ let!(:team) { club.teams.create!(name: "Tigers", 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.ago) }
+ let!(:session) do
+ StreamSession.create!(match: match, user: user, platform: "matchlivetv", status: "ended", ended_at: 1.hour.ago)
+ end
+ let!(:recording) do
+ Recording.create!(
+ stream_session: session, team: team, status: "ready", privacy_status: "public",
+ storage_key: "teams/#{team.id}/sessions/#{session.id}/replay.mp4",
+ expires_at: 2.days.from_now,
+ expiry_warning_sent_at: 1.day.ago
+ )
+ end
+
+ it "imposta una nuova data di scadenza e resetta l'avviso" do
+ new_expiry = 45.days.from_now
+
+ described_class.new(recording).to_time(new_expiry)
+
+ recording.reload
+ expect(recording.expires_at).to be_within(1.second).of(new_expiry)
+ expect(recording.expiry_warning_sent_at).to be_nil
+ end
+
+ it "prolunga dalla scadenza corrente" do
+ original = recording.expires_at
+
+ described_class.new(recording).by_days(30)
+
+ recording.reload
+ expect(recording.expires_at).to be_within(1.second).of(original + 30.days)
+ expect(recording.expiry_warning_sent_at).to be_nil
+ end
+
+ it "prolunga da oggi se la scadenza è già passata" do
+ recording.update!(expires_at: 2.days.ago)
+
+ described_class.new(recording).by_days(10)
+
+ recording.reload
+ expect(recording.expires_at).to be_within(1.second).of(10.days.from_now)
+ end
+
+ it "esclude il replay dalla coda di purge dopo il prolungamento" do
+ recording.update!(expires_at: 1.hour.ago)
+
+ expect(Recording.expired_pending_purge).to include(recording)
+
+ described_class.new(recording).by_days(30)
+
+ expect(Recording.expired_pending_purge).not_to include(recording.reload)
+ end
+
+ it "rifiuta date nel passato" do
+ expect do
+ described_class.new(recording).to_time(1.day.ago)
+ end.to raise_error(Recordings::ExtendExpiry::Error, /futuro/)
+ end
+end
|