diff --git a/apps/web/app/(org)/dashboard/caps/components/UploadCapButton.tsx b/apps/web/app/(org)/dashboard/caps/components/UploadCapButton.tsx
index 6062c4526..d94a0427e 100644
--- a/apps/web/app/(org)/dashboard/caps/components/UploadCapButton.tsx
+++ b/apps/web/app/(org)/dashboard/caps/components/UploadCapButton.tsx
@@ -9,7 +9,10 @@ import { useRef, useState } from "react";
import { toast } from "sonner";
import { createVideoAndGetUploadUrl } from "@/actions/video/upload";
import { useDashboardContext } from "@/app/(org)/dashboard/Contexts";
-import { useUploadingContext } from "@/app/(org)/dashboard/caps/UploadingContext";
+import {
+ type UploadStatus,
+ useUploadingContext,
+} from "@/app/(org)/dashboard/caps/UploadingContext";
import { UpgradeModal } from "@/components/UpgradeModal";
export const UploadCapButton = ({
@@ -43,468 +46,479 @@ export const UploadCapButton = ({
const file = e.target.files?.[0];
if (!file || !user) return;
- const parser = await import("@remotion/media-parser");
- const webcodecs = await import("@remotion/webcodecs");
-
- try {
- setUploadStatus({ status: "parsing" });
- const metadata = await parser.parseMedia({
- src: file,
- fields: {
- durationInSeconds: true,
- dimensions: true,
- fps: true,
- numberOfAudioChannels: true,
- sampleRate: true,
- },
- });
-
- const duration = metadata.durationInSeconds
- ? Math.round(metadata.durationInSeconds)
- : undefined;
-
- setUploadStatus({ status: "creating" });
- const videoData = await createVideoAndGetUploadUrl({
- duration,
- resolution: metadata.dimensions
- ? `${metadata.dimensions.width}x${metadata.dimensions.height}`
- : undefined,
- videoCodec: "h264",
- audioCodec: "aac",
- isScreenshot: false,
- isUpload: true,
- folderId,
- });
-
- const uploadId = videoData.id;
-
- setUploadStatus({ status: "converting", capId: uploadId, progress: 0 });
-
- let optimizedBlob: Blob;
-
- try {
- const calculateResizeOptions = () => {
- if (!metadata.dimensions) return undefined;
+ const ok = await legacyUploadCap(file, folderId, setUploadStatus);
+ if (ok) router.refresh();
+ if (inputRef.current) inputRef.current.value = "";
+ };
- const { width, height } = metadata.dimensions;
- const maxWidth = 1920;
- const maxHeight = 1080;
+ const isUploading = !!uploadStatus;
- if (width <= maxWidth && height <= maxHeight) {
- return undefined;
- }
+ return (
+ <>
+
+
+
+ >
+ );
+};
- const widthScale = maxWidth / width;
- const heightScale = maxHeight / height;
- const scale = Math.min(widthScale, heightScale);
+async function legacyUploadCap(
+ file: File,
+ folderId: string | undefined,
+ setUploadStatus: (state: UploadStatus | undefined) => void,
+) {
+ const parser = await import("@remotion/media-parser");
+ const webcodecs = await import("@remotion/webcodecs");
+
+ try {
+ setUploadStatus({ status: "parsing" });
+ const metadata = await parser.parseMedia({
+ src: file,
+ fields: {
+ durationInSeconds: true,
+ dimensions: true,
+ fps: true,
+ numberOfAudioChannels: true,
+ sampleRate: true,
+ },
+ });
+
+ const duration = metadata.durationInSeconds
+ ? Math.round(metadata.durationInSeconds)
+ : undefined;
+
+ setUploadStatus({ status: "creating" });
+ const videoData = await createVideoAndGetUploadUrl({
+ duration,
+ resolution: metadata.dimensions
+ ? `${metadata.dimensions.width}x${metadata.dimensions.height}`
+ : undefined,
+ videoCodec: "h264",
+ audioCodec: "aac",
+ isScreenshot: false,
+ isUpload: true,
+ folderId,
+ });
+
+ const uploadId = videoData.id;
+
+ setUploadStatus({ status: "converting", capId: uploadId, progress: 0 });
+
+ let optimizedBlob: Blob;
- return { mode: "scale" as const, scale };
- };
+ try {
+ const calculateResizeOptions = () => {
+ if (!metadata.dimensions) return undefined;
- const resizeOptions = calculateResizeOptions();
-
- const convertResult = await webcodecs.convertMedia({
- src: file,
- container: "mp4",
- videoCodec: "h264",
- audioCodec: "aac",
- ...(resizeOptions && { resize: resizeOptions }),
- onProgress: ({ overallProgress }) => {
- if (overallProgress !== null) {
- const progressValue = overallProgress * 100;
- setUploadStatus({
- status: "converting",
- capId: uploadId,
- progress: progressValue,
- });
- }
- },
- });
- optimizedBlob = await convertResult.save();
+ const { width, height } = metadata.dimensions;
+ const maxWidth = 1920;
+ const maxHeight = 1080;
- if (optimizedBlob.size === 0) {
- throw new Error("Conversion produced empty file");
+ if (width <= maxWidth && height <= maxHeight) {
+ return undefined;
}
- const isValidVideo = await new Promise((resolve) => {
- const testVideo = document.createElement("video");
- testVideo.muted = true;
- testVideo.playsInline = true;
- testVideo.preload = "metadata";
-
- const timeout = setTimeout(() => {
- console.warn("Video validation timed out");
- URL.revokeObjectURL(testVideo.src);
- resolve(false);
- }, 15000);
-
- let metadataLoaded = false;
-
- const validateVideo = () => {
- if (metadataLoaded) return;
- metadataLoaded = true;
- const hasValidDuration =
- testVideo.duration > 0 &&
- !isNaN(testVideo.duration) &&
- isFinite(testVideo.duration);
+ const widthScale = maxWidth / width;
+ const heightScale = maxHeight / height;
+ const scale = Math.min(widthScale, heightScale);
- const hasValidDimensions =
- (testVideo.videoWidth > 0 && testVideo.videoHeight > 0) ||
- (metadata.dimensions &&
- metadata.dimensions.width > 0 &&
- metadata.dimensions.height > 0);
-
- if (hasValidDuration && hasValidDimensions) {
- clearTimeout(timeout);
- URL.revokeObjectURL(testVideo.src);
- resolve(true);
- } else {
- console.warn(
- `Invalid video properties - Duration: ${testVideo.duration}, Dimensions: ${testVideo.videoWidth}x${testVideo.videoHeight}, Original dimensions: ${metadata.dimensions?.width}x${metadata.dimensions?.height}`,
- );
- clearTimeout(timeout);
- URL.revokeObjectURL(testVideo.src);
- resolve(false);
- }
- };
+ return { mode: "scale" as const, scale };
+ };
- testVideo.addEventListener("loadedmetadata", validateVideo);
- testVideo.addEventListener("loadeddata", validateVideo);
- testVideo.addEventListener("canplay", validateVideo);
+ const resizeOptions = calculateResizeOptions();
- testVideo.addEventListener("error", (e) => {
- console.error("Video validation error:", e);
+ const convertResult = await webcodecs.convertMedia({
+ src: file,
+ container: "mp4",
+ videoCodec: "h264",
+ audioCodec: "aac",
+ ...(resizeOptions && { resize: resizeOptions }),
+ onProgress: ({ overallProgress }) => {
+ if (overallProgress !== null) {
+ const progressValue = overallProgress * 100;
+ setUploadStatus({
+ status: "converting",
+ capId: uploadId,
+ progress: progressValue,
+ });
+ }
+ },
+ });
+ optimizedBlob = await convertResult.save();
+
+ if (optimizedBlob.size === 0)
+ throw new Error("Conversion produced empty file");
+ const isValidVideo = await new Promise((resolve) => {
+ const testVideo = document.createElement("video");
+ testVideo.muted = true;
+ testVideo.playsInline = true;
+ testVideo.preload = "metadata";
+
+ const timeout = setTimeout(() => {
+ console.warn("Video validation timed out");
+ URL.revokeObjectURL(testVideo.src);
+ resolve(false);
+ }, 15000);
+
+ let metadataLoaded = false;
+
+ const validateVideo = () => {
+ if (metadataLoaded) return;
+ metadataLoaded = true;
+
+ const hasValidDuration =
+ testVideo.duration > 0 &&
+ !isNaN(testVideo.duration) &&
+ isFinite(testVideo.duration);
+
+ const hasValidDimensions =
+ (testVideo.videoWidth > 0 && testVideo.videoHeight > 0) ||
+ (metadata.dimensions &&
+ metadata.dimensions.width > 0 &&
+ metadata.dimensions.height > 0);
+
+ if (hasValidDuration && hasValidDimensions) {
+ clearTimeout(timeout);
+ URL.revokeObjectURL(testVideo.src);
+ resolve(true);
+ } else {
+ console.warn(
+ `Invalid video properties - Duration: ${testVideo.duration}, Dimensions: ${testVideo.videoWidth}x${testVideo.videoHeight}, Original dimensions: ${metadata.dimensions?.width}x${metadata.dimensions?.height}`,
+ );
clearTimeout(timeout);
URL.revokeObjectURL(testVideo.src);
resolve(false);
- });
+ }
+ };
- testVideo.addEventListener("loadstart", () => {});
+ testVideo.addEventListener("loadedmetadata", validateVideo);
+ testVideo.addEventListener("loadeddata", validateVideo);
+ testVideo.addEventListener("canplay", validateVideo);
- testVideo.src = URL.createObjectURL(optimizedBlob);
+ testVideo.addEventListener("error", (e) => {
+ console.error("Video validation error:", e);
+ clearTimeout(timeout);
+ URL.revokeObjectURL(testVideo.src);
+ resolve(false);
});
- if (!isValidVideo) {
- throw new Error("Converted video is not playable");
- }
- } catch (conversionError) {
- console.error("Video conversion failed:", conversionError);
- toast.error(
- "Failed to process video file. This format may not be supported for upload.",
- );
- return;
+ testVideo.addEventListener("loadstart", () => {});
+
+ testVideo.src = URL.createObjectURL(optimizedBlob);
+ });
+
+ if (!isValidVideo) {
+ throw new Error("Converted video is not playable");
}
+ } catch (conversionError) {
+ console.error("Video conversion failed:", conversionError);
+ toast.error(
+ "Failed to process video file. This format may not be supported for upload.",
+ );
+ setUploadStatus(undefined);
+ return false;
+ }
- const captureThumbnail = (): Promise => {
- return new Promise((resolve) => {
- const video = document.createElement("video");
- video.src = URL.createObjectURL(optimizedBlob);
- video.muted = true;
- video.playsInline = true;
- video.crossOrigin = "anonymous";
+ const captureThumbnail = (): Promise => {
+ return new Promise((resolve) => {
+ const video = document.createElement("video");
+ video.src = URL.createObjectURL(optimizedBlob);
+ video.muted = true;
+ video.playsInline = true;
+ video.crossOrigin = "anonymous";
- const cleanup = () => {
- URL.revokeObjectURL(video.src);
- };
+ const cleanup = () => {
+ URL.revokeObjectURL(video.src);
+ };
- const timeout = setTimeout(() => {
+ const timeout = setTimeout(() => {
+ cleanup();
+ console.warn(
+ "Thumbnail generation timed out, proceeding without thumbnail",
+ );
+ resolve(null);
+ }, 10000);
+
+ video.addEventListener("loadedmetadata", () => {
+ try {
+ const seekTime = Math.min(1, video.duration / 4);
+ video.currentTime = seekTime;
+ } catch (err) {
+ console.warn("Failed to seek video for thumbnail:", err);
+ clearTimeout(timeout);
cleanup();
- console.warn(
- "Thumbnail generation timed out, proceeding without thumbnail",
- );
resolve(null);
- }, 10000);
-
- video.addEventListener("loadedmetadata", () => {
- try {
- const seekTime = Math.min(1, video.duration / 4);
- video.currentTime = seekTime;
- } catch (err) {
- console.warn("Failed to seek video for thumbnail:", err);
+ }
+ });
+
+ video.addEventListener("seeked", () => {
+ try {
+ const canvas = document.createElement("canvas");
+ canvas.width = video.videoWidth || 640;
+ canvas.height = video.videoHeight || 480;
+ const ctx = canvas.getContext("2d");
+ if (!ctx) {
+ console.warn("Failed to get canvas context");
clearTimeout(timeout);
cleanup();
resolve(null);
+ return;
}
- });
-
- video.addEventListener("seeked", () => {
- try {
- const canvas = document.createElement("canvas");
- canvas.width = video.videoWidth || 640;
- canvas.height = video.videoHeight || 480;
- const ctx = canvas.getContext("2d");
- if (!ctx) {
- console.warn("Failed to get canvas context");
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
+ canvas.toBlob(
+ (blob) => {
clearTimeout(timeout);
cleanup();
- resolve(null);
- return;
- }
- ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
- canvas.toBlob(
- (blob) => {
- clearTimeout(timeout);
- cleanup();
- if (blob) {
- resolve(blob);
- } else {
- console.warn("Failed to create thumbnail blob");
- resolve(null);
- }
- },
- "image/jpeg",
- 0.8,
- );
- } catch (err) {
- console.warn("Error during thumbnail capture:", err);
- clearTimeout(timeout);
- cleanup();
- resolve(null);
- }
- });
-
- video.addEventListener("error", (err) => {
- console.warn("Video loading error for thumbnail:", err);
+ if (blob) {
+ resolve(blob);
+ } else {
+ console.warn("Failed to create thumbnail blob");
+ resolve(null);
+ }
+ },
+ "image/jpeg",
+ 0.8,
+ );
+ } catch (err) {
+ console.warn("Error during thumbnail capture:", err);
clearTimeout(timeout);
cleanup();
resolve(null);
- });
+ }
+ });
- video.addEventListener("loadstart", () => {});
+ video.addEventListener("error", (err) => {
+ console.warn("Video loading error for thumbnail:", err);
+ clearTimeout(timeout);
+ cleanup();
+ resolve(null);
});
+
+ video.addEventListener("loadstart", () => {});
+ });
+ };
+
+ const thumbnailBlob = await captureThumbnail();
+ const thumbnailUrl = thumbnailBlob
+ ? URL.createObjectURL(thumbnailBlob)
+ : undefined;
+
+ const formData = new FormData();
+ Object.entries(videoData.presignedPostData.fields).forEach(
+ ([key, value]) => {
+ formData.append(key, value as string);
+ },
+ );
+ formData.append("file", optimizedBlob);
+
+ setUploadStatus({
+ status: "uploadingVideo",
+ capId: uploadId,
+ progress: 0,
+ thumbnailUrl,
+ });
+
+ // Create progress tracking state outside React
+ const createProgressTracker = () => {
+ const uploadState = {
+ videoId: uploadId,
+ uploaded: 0,
+ total: 0,
+ pendingTask: undefined as ReturnType | undefined,
+ lastUpdateTime: Date.now(),
};
- const thumbnailBlob = await captureThumbnail();
- const thumbnailUrl = thumbnailBlob
- ? URL.createObjectURL(thumbnailBlob)
- : undefined;
+ const scheduleProgressUpdate = (uploaded: number, total: number) => {
+ uploadState.uploaded = uploaded;
+ uploadState.total = total;
+ uploadState.lastUpdateTime = Date.now();
- const formData = new FormData();
- Object.entries(videoData.presignedPostData.fields).forEach(
- ([key, value]) => {
- formData.append(key, value as string);
- },
- );
- formData.append("file", optimizedBlob);
+ // Clear any existing pending task
+ if (uploadState.pendingTask) {
+ clearTimeout(uploadState.pendingTask);
+ uploadState.pendingTask = undefined;
+ }
- setUploadStatus({
- status: "uploadingVideo",
- capId: uploadId,
- progress: 0,
- thumbnailUrl,
- });
+ const shouldSendImmediately = uploaded >= total;
+
+ if (shouldSendImmediately) {
+ // Don't send completion update immediately - let xhr.onload handle it
+ // to avoid double progress updates
+ return;
+ } else {
+ // Schedule delayed update (after 2 seconds)
+ uploadState.pendingTask = setTimeout(() => {
+ if (uploadState.videoId) {
+ sendProgressUpdate(
+ uploadState.videoId,
+ uploadState.uploaded,
+ uploadState.total,
+ );
+ }
+ uploadState.pendingTask = undefined;
+ }, 2000);
+ }
+ };
- // Create progress tracking state outside React
- const createProgressTracker = () => {
- const uploadState = {
- videoId: uploadId,
- uploaded: 0,
- total: 0,
- pendingTask: undefined as ReturnType | undefined,
- lastUpdateTime: Date.now(),
- };
+ const cleanup = () => {
+ if (uploadState.pendingTask) {
+ clearTimeout(uploadState.pendingTask);
+ uploadState.pendingTask = undefined;
+ }
+ };
- const scheduleProgressUpdate = (uploaded: number, total: number) => {
- uploadState.uploaded = uploaded;
- uploadState.total = total;
- uploadState.lastUpdateTime = Date.now();
+ const getTotal = () => uploadState.total;
- // Clear any existing pending task
- if (uploadState.pendingTask) {
- clearTimeout(uploadState.pendingTask);
- uploadState.pendingTask = undefined;
- }
+ return { scheduleProgressUpdate, cleanup, getTotal };
+ };
- const shouldSendImmediately = uploaded >= total;
+ const progressTracker = createProgressTracker();
- if (shouldSendImmediately) {
- // Don't send completion update immediately - let xhr.onload handle it
- // to avoid double progress updates
- return;
- } else {
- // Schedule delayed update (after 2 seconds)
- uploadState.pendingTask = setTimeout(() => {
- if (uploadState.videoId) {
- sendProgressUpdate(
- uploadState.videoId,
- uploadState.uploaded,
- uploadState.total,
- );
- }
- uploadState.pendingTask = undefined;
- }, 2000);
+ try {
+ await new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+ xhr.open("POST", videoData.presignedPostData.url);
+
+ xhr.upload.onprogress = (event) => {
+ if (event.lengthComputable) {
+ const percent = (event.loaded / event.total) * 100;
+ setUploadStatus({
+ status: "uploadingVideo",
+ capId: uploadId,
+ progress: percent,
+ thumbnailUrl,
+ });
+
+ progressTracker.scheduleProgressUpdate(event.loaded, event.total);
}
};
- const cleanup = () => {
- if (uploadState.pendingTask) {
- clearTimeout(uploadState.pendingTask);
- uploadState.pendingTask = undefined;
+ xhr.onload = () => {
+ if (xhr.status >= 200 && xhr.status < 300) {
+ progressTracker.cleanup();
+ // Guarantee final 100% progress update
+ const total = progressTracker.getTotal() || 1;
+ sendProgressUpdate(uploadId, total, total);
+ resolve();
+ } else {
+ progressTracker.cleanup();
+ reject(new Error(`Upload failed with status ${xhr.status}`));
}
};
+ xhr.onerror = () => {
+ progressTracker.cleanup();
+ reject(new Error("Upload failed"));
+ };
- const getTotal = () => uploadState.total;
-
- return { scheduleProgressUpdate, cleanup, getTotal };
- };
-
- const progressTracker = createProgressTracker();
-
- try {
- await new Promise((resolve, reject) => {
- const xhr = new XMLHttpRequest();
- xhr.open("POST", videoData.presignedPostData.url);
-
- xhr.upload.onprogress = (event) => {
- if (event.lengthComputable) {
- const percent = (event.loaded / event.total) * 100;
- setUploadStatus({
- status: "uploadingVideo",
- capId: uploadId,
- progress: percent,
- thumbnailUrl,
- });
-
- progressTracker.scheduleProgressUpdate(event.loaded, event.total);
- }
- };
-
- xhr.onload = () => {
- if (xhr.status >= 200 && xhr.status < 300) {
- progressTracker.cleanup();
- // Guarantee final 100% progress update
- const total = progressTracker.getTotal() || 1;
- sendProgressUpdate(uploadId, total, total);
- resolve();
- } else {
- progressTracker.cleanup();
- reject(new Error(`Upload failed with status ${xhr.status}`));
- }
- };
- xhr.onerror = () => {
- progressTracker.cleanup();
- reject(new Error("Upload failed"));
- };
-
- xhr.send(formData);
- });
- } catch (uploadError) {
- progressTracker.cleanup();
- throw uploadError;
- }
+ xhr.send(formData);
+ });
+ } catch (uploadError) {
+ progressTracker.cleanup();
+ throw uploadError;
+ }
- if (thumbnailBlob) {
- const screenshotData = await createVideoAndGetUploadUrl({
- videoId: uploadId,
- isScreenshot: true,
- isUpload: true,
- });
+ if (thumbnailBlob) {
+ const screenshotData = await createVideoAndGetUploadUrl({
+ videoId: uploadId,
+ isScreenshot: true,
+ isUpload: true,
+ });
- const screenshotFormData = new FormData();
- Object.entries(screenshotData.presignedPostData.fields).forEach(
- ([key, value]) => {
- screenshotFormData.append(key, value as string);
- },
- );
- screenshotFormData.append("file", thumbnailBlob);
-
- setUploadStatus({
- status: "uploadingThumbnail",
- capId: uploadId,
- progress: 0,
- });
- await new Promise((resolve, reject) => {
- const xhr = new XMLHttpRequest();
- xhr.open("POST", screenshotData.presignedPostData.url);
-
- xhr.upload.onprogress = (event) => {
- if (event.lengthComputable) {
- const percent = (event.loaded / event.total) * 100;
- const thumbnailProgress = 90 + percent * 0.1;
- setUploadStatus({
- status: "uploadingThumbnail",
- capId: uploadId,
- progress: thumbnailProgress,
- });
- }
- };
-
- xhr.onload = () => {
- if (xhr.status >= 200 && xhr.status < 300) {
- resolve();
- } else {
- reject(
- new Error(`Screenshot upload failed with status ${xhr.status}`),
- );
- }
- };
- xhr.onerror = () => reject(new Error("Screenshot upload failed"));
+ const screenshotFormData = new FormData();
+ Object.entries(screenshotData.presignedPostData.fields).forEach(
+ ([key, value]) => {
+ screenshotFormData.append(key, value as string);
+ },
+ );
+ screenshotFormData.append("file", thumbnailBlob);
- xhr.send(screenshotFormData);
- });
- }
+ setUploadStatus({
+ status: "uploadingThumbnail",
+ capId: uploadId,
+ progress: 0,
+ });
+ await new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+ xhr.open("POST", screenshotData.presignedPostData.url);
+
+ xhr.upload.onprogress = (event) => {
+ if (event.lengthComputable) {
+ const percent = (event.loaded / event.total) * 100;
+ const thumbnailProgress = 90 + percent * 0.1;
+ setUploadStatus({
+ status: "uploadingThumbnail",
+ capId: uploadId,
+ progress: thumbnailProgress,
+ });
+ }
+ };
- router.refresh();
- } catch (err) {
- console.error("Video upload failed", err);
- } finally {
- setUploadStatus(undefined);
- if (inputRef.current) inputRef.current.value = "";
- }
- };
+ xhr.onload = () => {
+ if (xhr.status >= 200 && xhr.status < 300) {
+ resolve();
+ } else {
+ reject(
+ new Error(`Screenshot upload failed with status ${xhr.status}`),
+ );
+ }
+ };
+ xhr.onerror = () => reject(new Error("Screenshot upload failed"));
- const sendProgressUpdate = async (
- videoId: string,
- uploaded: number,
- total: number,
- ) => {
- try {
- const response = await fetch("/api/desktop/video/progress", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- videoId,
- uploaded,
- total,
- updatedAt: new Date().toISOString(),
- }),
+ xhr.send(screenshotFormData);
});
-
- if (!response.ok)
- console.error("Failed to send progress update:", response.status);
- } catch (err) {
- console.error("Error sending progress update:", err);
}
- };
-
- const isUploading = !!uploadStatus;
- return (
- <>
-
-
-
- >
- );
+ setUploadStatus(undefined);
+ return true;
+ } catch (err) {
+ console.error("Video upload failed", err);
+ }
+
+ setUploadStatus(undefined);
+ return false;
+}
+
+const sendProgressUpdate = async (
+ videoId: string,
+ uploaded: number,
+ total: number,
+) => {
+ try {
+ const response = await fetch("/api/desktop/video/progress", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ videoId,
+ uploaded,
+ total,
+ updatedAt: new Date().toISOString(),
+ }),
+ });
+
+ if (!response.ok)
+ console.error("Failed to send progress update:", response.status);
+ } catch (err) {
+ console.error("Error sending progress update:", err);
+ }
};