feat(media): delete videos from the media library

Add an admin flow to permanently delete a media asset and clean up
everything it leaves behind.

- Delete dialog with a destructive, irreversible confirmation; deletion
  is blocked while the video is still attached and shows the in-use
  lessons so the admin can detach first.
- Usage list resolves lesson/course context for each attachment, links
  to the lesson editor via an anchor (right-click "open in new tab"),
  and exposes a refresh button to re-check usage after detaching
  elsewhere.
- Storage cleanup runs as a background maintenance job: the producer
  captures the asset's object keys/prefixes up front and an idempotent
  worker sweeps HLS segments, thumbnails, audio, transcript, and the
  raw upload from object storage.

Also harden usage accounting so stale attachments cannot trap an asset:

- asset_usages has no FK to its polymorphic target, so prune a lesson's
  usage rows whenever the lesson is hard-deleted (single + bulk paths).
- Usage resolution drops orphaned lesson usages (lesson gone or course
  soft-deleted) so they neither appear in the list nor block deletion.

Document the icon-only button convention (variant="secondary") in
AGENTS.md.
This commit is contained in:
Rotimi Best
2026-06-28 12:36:13 +01:00
parent 2ccbf30414
commit 08ea608f94
31 changed files with 966 additions and 80 deletions
@@ -357,7 +357,11 @@
"thumbnail_regen_failed": "Falha ao regenerar miniaturas",
"transcript_saved": "Transcrição atualizada",
"transcript_save_failed": "Não foi possível salvar a transcrição",
"transcription_already_running": "Um trabalho de transcrição já está em andamento para este vídeo"
"transcription_already_running": "Um trabalho de transcrição já está em andamento para este vídeo",
"video_added_to_lesson": "Vídeo adicionado à aula",
"delete_success": "Vídeo excluído",
"delete_in_use": "Este vídeo ainda está anexado a uma aula. Remova-o primeiro.",
"delete_failed": "Falha ao excluir vídeo"
},
"tags": {
"group_created": "Grupo de tags criado",
@@ -2808,7 +2812,14 @@
"exercise": "Exercício",
"question": "Pergunta",
"unknown": "Desconhecido"
}
},
"go_to_lesson": "Vá para a aula",
"slot_type": {
"lesson_video": "Vídeo da aula",
"lesson_document": "Documento da lição",
"unknown": "Anexo"
},
"refresh": "Atualizar uso"
},
"form": {
"file_name": "Nome do arquivo",
@@ -2838,7 +2849,8 @@
"edit": "Editar",
"where_used": "Onde usado",
"download": "Baixar",
"manage_thumbnails": "Gerenciar miniaturas"
"manage_thumbnails": "Gerenciar miniaturas",
"delete": "Excluir"
},
"table": {
"title": "Título",
@@ -2910,6 +2922,16 @@
"custom_preview_alt": "Pré-visualização de miniaturas personalizadas",
"regenerate": "Regenerar",
"save_selection": "Salvar miniatura"
},
"delete": {
"title": "Excluir vídeo",
"irreversible_title": "Isso não pode ser desfeito",
"irreversible_description": "Este vídeo e todos os seus arquivos – representações, miniaturas e legendas – serão excluídos permanentemente e não poderão ser recuperados.",
"in_use_title": "Ainda em uso",
"in_use_description": "Este vídeo está anexado às aulas abaixo. Abra cada lição, remova o vídeo e volte para excluí-lo.",
"confirm": "Excluir permanentemente",
"cancel": "Cancelar",
"close": "Fechar"
}
},
"settings": {