feat: add video job cancellation functionality and error tracking in generated videos

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-04-29 20:04:10 +02:00
parent 3d0a08a8ef
commit 299ad7d943
13 changed files with 282 additions and 61 deletions
+79 -15
View File
@@ -18,23 +18,34 @@ content %}
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
>
{% for video in pending_videos %}
<a
href="{{ url_for('video_detail', video_id=video.id) }}"
class="block bg-gray-800 rounded-lg shadow-lg overflow-hidden hover:shadow-2xl transition-shadow duration-300"
<div
class="block bg-gray-800 rounded-lg shadow-lg overflow-hidden hover:shadow-2xl transition-shadow duration-300 relative"
data-pending-video-id="{{ video.id }}"
>
<div class="p-4">
<p class="font-bold text-lg truncate">{{ video.prompt }}</p>
<p class="text-sm text-gray-400">
Video Job Status:
<span class="font-semibold text-yellow-400"
>{{ video.status }}</span
>
</p>
<p class="text-xs text-gray-500 mt-2">
Started: {{ video.created_at | fromisoformat | humantime }}
</p>
<a href="{{ url_for('video_detail', video_id=video.id) }}">
<div class="p-4">
<p class="font-bold text-lg truncate">{{ video.prompt }}</p>
<p class="text-sm text-gray-400">
Video Job Status:
<span class="font-semibold text-yellow-400"
>{{ video.status }}</span
>
</p>
<p class="text-xs text-gray-500 mt-2">
Started: {{ video.created_at | fromisoformat | humantime }}
</p>
</div>
</a>
<div class="px-4 pb-4">
<button
class="cancel-pending-btn px-3 py-1 bg-red-600 hover:bg-red-700 text-white rounded text-xs"
data-video-id="{{ video.id }}"
>
Cancel
</button>
<span class="cancel-pending-msg text-xs ml-2 hidden"></span>
</div>
</a>
</div>
{% endfor %}
</div>
</div>
@@ -230,6 +241,59 @@ content %}
}, 1500);
}
});
// Cancel pending video buttons
document.querySelectorAll(".cancel-pending-btn").forEach((btn) => {
btn.addEventListener("click", async (e) => {
e.preventDefault();
e.stopPropagation();
const videoId = btn.dataset.videoId;
const msgEl = btn.parentElement.querySelector(".cancel-pending-msg");
btn.disabled = true;
btn.textContent = "Cancelling…";
try {
const resp = await fetch(
"/generate/video/" + encodeURIComponent(videoId) + "/cancel",
{ method: "POST" },
);
if (resp.ok) {
btn.classList.add("hidden");
if (msgEl) {
msgEl.textContent = "Cancelled";
msgEl.classList.remove("hidden", "text-red-500");
msgEl.classList.add("text-gray-300");
}
const card = document.querySelector(
'[data-pending-video-id="' + videoId + '"]',
);
if (card) {
const statusSpan = card.querySelector(".text-yellow-400");
if (statusSpan) {
statusSpan.textContent = "cancelled";
statusSpan.classList.remove("text-yellow-400");
statusSpan.classList.add("text-gray-400");
}
}
} else {
const data = await resp.json().catch(() => ({}));
btn.disabled = false;
btn.textContent = "Cancel";
if (msgEl) {
msgEl.textContent = data.detail || "Failed";
msgEl.classList.remove("hidden");
msgEl.classList.add("text-red-500");
}
}
} catch (err) {
btn.disabled = false;
btn.textContent = "Cancel";
if (msgEl) {
msgEl.textContent = "Error";
msgEl.classList.remove("hidden");
msgEl.classList.add("text-red-500");
}
}
});
});
});
</script>
{% endblock %}