diff --git a/src/app/(protected)/workorders/_components/media/media-uploader.tsx b/src/app/(protected)/workorders/_components/media/media-uploader.tsx index c87f430e..5364bd7b 100644 --- a/src/app/(protected)/workorders/_components/media/media-uploader.tsx +++ b/src/app/(protected)/workorders/_components/media/media-uploader.tsx @@ -45,7 +45,7 @@ const VIDEO_DURATION_MESSAGE = `Videos must be ${MAX_VIDEO_DURATION_SECONDS} sec async function isWithinContract(file: File, kind: WorkOrderUploadKind): Promise { if (kind === "video") { - // SH-116: duration is checked client-side only; unreadable metadata never blocks. + // SH-116: pre-check only; the server enforces it too. Unreadable metadata never blocks. const duration = await probeVideoDurationSeconds(file); if (duration != null && duration > MAX_VIDEO_DURATION_SECONDS) { toast.error(VIDEO_DURATION_MESSAGE); diff --git a/src/domain/vendor-portal/lib/document-validation.ts b/src/domain/vendor-portal/lib/document-validation.ts index 81444abc..bf059bca 100644 --- a/src/domain/vendor-portal/lib/document-validation.ts +++ b/src/domain/vendor-portal/lib/document-validation.ts @@ -56,7 +56,7 @@ export async function validateVendorDocument(file: File): Promise MAX_VIDEO_BYTES) { return "Videos must be 100 MB or smaller."; } - // Duration is checked client-side only; unreadable metadata never blocks. + // Pre-check; the server enforces it too. Unreadable metadata never blocks. const duration = await probeVideoDurationSeconds(file); if (duration != null && duration > MAX_VIDEO_DURATION_SECONDS) { return `Videos must be ${MAX_VIDEO_DURATION_SECONDS} seconds or shorter.`; diff --git a/src/domain/work-orders/utils/work-order-media-limits.ts b/src/domain/work-orders/utils/work-order-media-limits.ts index 77429d75..e19640d5 100644 --- a/src/domain/work-orders/utils/work-order-media-limits.ts +++ b/src/domain/work-orders/utils/work-order-media-limits.ts @@ -5,7 +5,8 @@ import { isWorkOrderCompletionSurfaceReadOnly } from "@/domain/work-orders/utils * (JPEG/PNG/HEIC), videos up to 100 MB and 90 seconds (MP4/MOV), at most * 10 photos and 3 videos per work order. Applies to the Photos & Videos modal, * the Completion Doc → Media tab and the vendor portal upload. Duration is - * checked client-side only (the server cannot probe it cheaply). Documents + * pre-checked here and enforced by the server from the MP4/MOV header; + * unreadable metadata never blocks. Documents * keep the 50 MB document cap; the signed completion PDF keeps its own cap. */ export const MAX_WORK_ORDER_PHOTO_BYTES = 10_000_000; diff --git a/src/lib/probe-video-duration.ts b/src/lib/probe-video-duration.ts index 61f047f6..d09bec1c 100644 --- a/src/lib/probe-video-duration.ts +++ b/src/lib/probe-video-duration.ts @@ -1,7 +1,8 @@ /** * Best-effort client-side video duration probe. Resolves null when the browser * cannot decode the container's metadata — callers must not block the upload in - * that case (SH-116: duration is a client-side check only). + * that case (SH-116: the server enforces the limit from the MP4/MOV header and + * also lets unreadable metadata through). */ export const VIDEO_DURATION_PROBE_TIMEOUT_MS = 10_000;