|
|
@@ -7,11 +7,14 @@ interface Props {
|
|
|
assets: Asset[];
|
|
|
token: string | null;
|
|
|
canManage: boolean;
|
|
|
+ isAdmin: boolean;
|
|
|
onDelete: (id: string, title: string) => void;
|
|
|
onCancel: (id: string) => void;
|
|
|
onPause: (id: string) => void;
|
|
|
onResume: (id: string) => void;
|
|
|
onReprocess: (id: string) => void;
|
|
|
+ onReprocessAll: () => void;
|
|
|
+ isReprocessingAll: boolean;
|
|
|
}
|
|
|
|
|
|
const STATUS_CONFIG: Record<TranscodeStatus, {
|
|
|
@@ -246,7 +249,7 @@ function TranscodeTaskRow({
|
|
|
);
|
|
|
}
|
|
|
|
|
|
-export function TranscodeTasksPanel({ assets, canManage, onDelete, onCancel, onPause, onResume, onReprocess }: Props) {
|
|
|
+export function TranscodeTasksPanel({ assets, canManage, isAdmin, onDelete, onCancel, onPause, onResume, onReprocess, onReprocessAll, isReprocessingAll }: Props) {
|
|
|
const [filter, setFilter] = useState<'all' | 'processing' | 'completed' | 'failed'>('all');
|
|
|
|
|
|
const filtered = assets.filter(a => {
|
|
|
@@ -260,6 +263,7 @@ export function TranscodeTasksPanel({ assets, canManage, onDelete, onCancel, onP
|
|
|
const processingCount = assets.filter(a => ['PENDING', 'UPLOADING', 'PROCESSING'].includes(a.transcodeStatus)).length;
|
|
|
const completedCount = assets.filter(a => a.transcodeStatus === 'COMPLETED').length;
|
|
|
const failedCount = assets.filter(a => ['FAILED', 'UNSUPPORTED_CODEC'].includes(a.transcodeStatus)).length;
|
|
|
+ const stuckCount = assets.filter(a => a.transcodeStatus === 'PROCESSING').length;
|
|
|
|
|
|
return (
|
|
|
<div>
|
|
|
@@ -290,6 +294,37 @@ export function TranscodeTasksPanel({ assets, canManage, onDelete, onCancel, onP
|
|
|
))}
|
|
|
</div>
|
|
|
|
|
|
+ {/* Admin: Force Reprocess All */}
|
|
|
+ {isAdmin && (
|
|
|
+ <div className="flex justify-end mb-4">
|
|
|
+ <button
|
|
|
+ onClick={onReprocessAll}
|
|
|
+ disabled={isReprocessingAll || stuckCount === 0}
|
|
|
+ className="flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-medium transition-all disabled:opacity-40"
|
|
|
+ style={{
|
|
|
+ background: 'rgba(251,146,60,0.10)',
|
|
|
+ color: '#FB923C',
|
|
|
+ border: '1px solid rgba(251,146,60,0.20)',
|
|
|
+ }}
|
|
|
+ title="Reset all stuck PROCESSING jobs to PENDING"
|
|
|
+ >
|
|
|
+ {isReprocessingAll ? (
|
|
|
+ <div className="w-3.5 h-3.5 border-2 border-current border-t-transparent rounded-full animate-spin" />
|
|
|
+ ) : (
|
|
|
+ <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
|
+ <path strokeLinecap="round" strokeLinejoin="round" d="M19.5 12c0-1.232-.046-2.453-.138-3.662a4.006 4.006 0 00-3.7-3.7 48.678 48.678 0 00-7.324 0 4.006 4.006 0 00-3.7 3.7c-.017.22-.032.441-.046.662M19.5 12l3-3m-3 3l-3-3m-12 3c0 1.232.046 2.453.138 3.662a4.006 4.006 0 003.7 3.7 48.656 48.656 0 007.324 0 4.006 4.006 0 003.7-3.7c.017-.22.032-.441.046-.662M4.5 12l3 3m-3-3l-3 3" />
|
|
|
+ </svg>
|
|
|
+ )}
|
|
|
+ <span>{isReprocessingAll ? 'Resetting…' : 'Force Reprocess All'}</span>
|
|
|
+ {stuckCount > 0 && !isReprocessingAll && (
|
|
|
+ <span className="text-[10px] px-1.5 py-0.5 rounded-full" style={{ background: 'rgba(251,146,60,0.20)' }}>
|
|
|
+ {stuckCount} stuck
|
|
|
+ </span>
|
|
|
+ )}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
{/* Task list */}
|
|
|
{filtered.length === 0 ? (
|
|
|
<div className="card p-16 text-center">
|